foldrun 2.8.1 → 2.9.1

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
@@ -178,6 +178,7 @@ const HELP = `foldrun — agents are just folders
178
178
  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
179
  foldrun observe where the account fails, retries and spends, per workspace (--to <workspace> in full, --since <days>, --json)
180
180
  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)
181
182
  foldrun schedule every flow that fires on a clock, its cron line and the next times (--to <workspace>)
182
183
  foldrun schedule tick fire whatever is due now — Settings' "Run scheduler now"
183
184
  foldrun guide write the coding-agent rules: a block in AGENTS.md and CLAUDE.md importing it (--check, --print)
package/docs/agents.md CHANGED
@@ -346,14 +346,78 @@ What holds:
346
346
  | field | |
347
347
  |---|---|
348
348
  | `size` | `small` \| `large` (default) \| `heavy` — the sandbox reservation |
349
+ | `resources` | `{cpu, memory, disk}` in numbers, over the `size` class's figures one key at a time |
349
350
  | `runtime` | language runtimes, packages and Debian system packages the agent's own scripts need |
350
351
 
351
- `size` is a price, not just a limit. Memory is a **hard ceiling** because it is
352
- not compressible: a step over it is killed, which on a single-node host
353
- protects every other tenant. CPU is not capped — a step bursts to whatever is
354
- free and bills at its class rate. Reference figures: 1Gi, 2Gi, 8Gi. Pick
355
- `small` for an agent that mostly waits on an API, `heavy` for one that renders
356
- a browser or holds real data.
352
+ `size` is a price and a limit. Each class gives the sandbox a number of cores,
353
+ an amount of memory and an amount of disk, and its seconds are billed at those
354
+ figures:
355
+
356
+ | `size` | cores | memory | disk |
357
+ |---|---|---|---|
358
+ | `small` | 1 | 1Gi | 2Gi |
359
+ | `large` | 2 | 2Gi | 8Gi |
360
+ | `heavy` | 4 | 8Gi | 20Gi |
361
+
362
+ These are the defaults; an install can set its own
363
+ ([environment](environment)), and the step's log says what it held.
364
+
365
+ All three are **hard ceilings**:
366
+
367
+ - **Memory:** a step over it is killed, because memory is not compressible.
368
+ On a single-node host, that protects every other tenant.
369
+ - **Cores:** CPU is capped at the class's figure, so a step cannot take the
370
+ whole machine from the steps beside it. When every core is busy, a bigger
371
+ class gets a bigger share.
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.
377
+
378
+ Pick `small` for an agent that mostly waits on an API, `heavy` for one that
379
+ renders a browser or holds real data. The step's `resources:` line reports CPU
380
+ busy time, **throttled** time (how long the cap held it back), and peak memory.
381
+ A step that is throttled for much of its run wants a bigger class; one whose
382
+ peak memory is a fraction of the class's figure can take a smaller one.
383
+
384
+ ### `resources:` — your own numbers
385
+
386
+ When no class fits, write the numbers. Each key replaces that one figure of
387
+ the class; the rest stay the class's:
388
+
389
+ ```yaml
390
+ size: large
391
+ resources:
392
+ cpu: 1 # cores, 0.5 to 8, in halves
393
+ memory: 6Gi # 256Mi to 16Gi, in steps of 256Mi
394
+ disk: 30Gi # whole GiB, 1Gi to 40Gi
395
+ ```
396
+
397
+ Examples:
398
+
399
+ - One core and a lot of memory, for an agent that loads a big CSV.
400
+ - A lot of disk, for one that renders video.
401
+
402
+ The figures are hard limits and what the step is billed at, exactly as for a
403
+ class. The step's header on the run page shows what it held ("1 vCPU ·
404
+ 6 GiB"), with the class and your `resources:` in its tooltip.
405
+
406
+ The keys are bounded and stepped the way sandbox platforms sell them, so
407
+ steps pack onto a machine predictably. `foldrun check` reports a value
408
+ outside them (`config/invalid-value`); at run time that one key is dropped,
409
+ the class's figure is used, and the step's log says so. Sizes need a unit:
410
+ `4Gi` or `512Mi`, never a bare `4`, and not `4G` (Kubernetes reads `G` as
411
+ decimal, Docker as binary).
412
+
413
+ `resources:` may also be written inside `guardrail:` ([four jobs](four-jobs)).
414
+ When a retry moves a step up a class after it ran out of memory or disk, each
415
+ of your figures is raised to the bigger class's, so the retry never runs
416
+ smaller than the class it moved to.
417
+
418
+ Your account's plan may also cap what one step can hold, and how much all of
419
+ an account's running steps hold together
420
+ ([budgets and billing](budgets-and-billing)).
357
421
 
358
422
  ```yaml
359
423
  runtime:
package/docs/api.md CHANGED
@@ -340,6 +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
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 |
344
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 |
345
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 |
@@ -358,7 +359,7 @@ Reads return which secrets exist, never their values.
358
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) |
359
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) |
360
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 |
361
- | `/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`, `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 }` — 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
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 |
363
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 |
364
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 |
@@ -427,7 +428,7 @@ X-Foldrun-Cli: 0.6.0 # the CLI released with this version (absent
427
428
 
428
429
  | Route | Methods | |
429
430
  |---|---|---|
430
- | `/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 |
431
432
  | `/api/auth/login` | POST | `email`, `password` |
432
433
  | `/api/auth/logout` | POST | ends the session |
433
434
  | `/api/auth/session` | GET | who am I |
@@ -27,6 +27,7 @@ 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 |
30
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 |
31
32
 
32
33
  A cap is a literal, never an expression, so it is readable off the file. The
@@ -64,12 +65,18 @@ at the platform's rates. The ledger stays in dollars underneath — every
64
65
  statement, budget and refund reconciles to one column — and a credit is how
65
66
  the money is shown and bought.
66
67
 
67
- | plan | a month | credits a month | an extra credit | runs at once | workspaces | queue lane |
68
- |---|---|---|---|---|---|---|
69
- | **Starter** | $29 | 2,900 | 1.00¢ | 1 | 3 | standard |
70
- | **Creator** | $79 | 8,800 | 0.90¢ | 2 | 10 | standard |
71
- | **Pro** | $249 | 29,000 | 0.86¢ | 4 | unlimited | fast |
72
- | **Scale** | $799 | 100,000 | 0.80¢ | 8 | unlimited | fast |
68
+ | plan | a month | credits a month | an extra credit | runs at once | compute at once | one step, at most | workspaces | queue lane |
69
+ |---|---|---|---|---|---|---|---|---|
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 |
74
+
75
+ **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.
73
80
 
74
81
  **How long a credit lasts.** A plan's credits land when its invoice is paid,
75
82
  and what is unused **rolls over** into the next cycle up to **twice** the
package/docs/four-jobs.md CHANGED
@@ -37,8 +37,8 @@ top. Either way works, and the two can be mixed in one file.
37
37
 
38
38
  | File | `governance:` | `guardrail:` | `observability:` | `evaluation:` |
39
39
  |---|---|---|---|---|
40
- | `AGENTS.md` | `owner`, `budget`, `priority`, `concurrency`, `deploy_block` | `limits`, `network`, `network_enforce`, `hooks` | `alerts`, `telemetry`, `notify`, `capture` | `judge` |
41
- | an agent | `owner`, `budget` | `limits`, `network`, `hooks`, `disallowedTools`, `permissionMode`, `secrets`, `sandbox` | — | — |
40
+ | `AGENTS.md` | `owner`, `budget`, `priority`, `concurrency`, `compute`, `deploy_block` | `limits`, `network`, `network_enforce`, `hooks` | `alerts`, `telemetry`, `notify`, `capture` | `judge` |
41
+ | an agent | `owner`, `budget` | `limits`, `network`, `hooks`, `disallowedTools`, `permissionMode`, `secrets`, `sandbox`, `resources` | — | — |
42
42
  | a flow | `owner`, `approvers`, `approve_within`, `budget`, `priority` | `throttle`, `debounce`, `overlap`, `disable_after`, `live` | `issues`, `sla` | — |
43
43
 
44
44
  A field that does none of the four, such as `name`, `model`, `tools`,
@@ -23,36 +23,146 @@ A self-hosted server has no billing: nothing is charged, and spend budgets
23
23
 
24
24
  ## Your own server
25
25
 
26
- One machine with Docker. The platform runs in one container — the dashboard,
27
- the API, the scheduler and the queue — and every agent step runs in a
28
- throwaway container beside it, started through the Docker socket.
26
+ One machine with Docker — Linux or macOS, Intel/AMD (amd64) or ARM (arm64);
27
+ on Windows, inside WSL2 with Docker Desktop's WSL integration. One command:
29
28
 
30
29
  ```sh
31
- mkdir foldrun && cd foldrun
32
- for r in foldrun-core foldrun-platform foldrun-web foldrun-docs; do
33
- git clone https://github.com/foldrun-io/$r
34
- done
35
- cd foldrun-web/self-hosting
36
- echo "FOLDRUN_SECRET_KEY=$(openssl rand -hex 32)" > .env
37
- echo "FOLDRUN_PUBLIC_URL=http://<host>:3900" >> .env # the address people open
38
- docker compose up -d --build
30
+ curl -fsSL https://foldrun.io/install.sh | sh
39
31
  ```
40
32
 
41
- Then open `http://<host>:3900/signup` (the first account needs no flag),
42
- sign the CLI in to it, and deploy an account folder:
33
+ It checks Docker, puts `docker-compose.yml` in `~/foldrun`, writes `.env`
34
+ there once (a fresh secret key, the address, the version), pulls the
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:
43
81
 
44
82
  ```sh
45
83
  npm install -g foldrun
46
- foldrun login --url http://<host>:3900 # approve in the browser; or --token <a key from Settings>
47
- foldrun deploy # from the account folder
84
+ foldrun login --url http://localhost:3900
85
+ foldrun deploy # from your account folder
48
86
  ```
49
87
 
50
- `docker-compose.yml` beside the Dockerfile lists every setting with what it
51
- does: the install's model key (or none, and each account brings its own),
52
- open sign-up (off), retention. `FOLDRUN_PUBLIC_URL` is required: CLI sign-in,
53
- approve links, password resets and shares are made with it, so it must be
54
- the address people actually open. Back up the `foldrun-data` volume and `FOLDRUN_SECRET_KEY` and
55
- you have backed the install up.
88
+ To read the script before it runs (anything piped into a shell deserves it):
89
+ `curl -fsSL https://foldrun.io/install.sh -o install.sh`, read it, then
90
+ `sh install.sh`. Its source is `self-hosting/install.sh` in
91
+ [foldrun-web](https://github.com/foldrun-io/foldrun-web/tree/main/self-hosting).
92
+
93
+ The platform runs in one container — the dashboard, the API, the scheduler
94
+ and the queue — and every agent step runs in a throwaway container beside
95
+ it, started through the Docker socket. The images are
96
+ `ghcr.io/foldrun-io/platform` and `ghcr.io/foldrun-io/runner`, public, built
97
+ from these repositories.
98
+
99
+ ### Settings
100
+
101
+ Give them to the installer as environment variables
102
+ (`curl -fsSL https://foldrun.io/install.sh | FOLDRUN_BIND=0.0.0.0 sh`) or
103
+ edit `~/foldrun/.env` and run `docker compose up -d` there.
104
+
105
+ | variable | default | what |
106
+ |---|---|---|
107
+ | `FOLDRUN_PUBLIC_URL` | `http://localhost:3900` | the address people open. CLI sign-in, approve links, password resets and shares are made with it |
108
+ | `FOLDRUN_BIND` | `127.0.0.1` | where the dashboard listens. The default is this machine only — reach it over an SSH tunnel or a reverse proxy. `0.0.0.0` opens it to the network: sign up straight away, because the first account becomes the owner |
109
+ | `FOLDRUN_VERSION` | the current `stable` | a version to pin, `vYYYY.MM.DD.N`; `stable` unpins. Give it to the installer: in `.env` by hand it holds only with `FOLDRUN_PINNED=yes` beside it, or the next installer run moves it to `stable` |
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 |
111
+ | `FOLDRUN_DIR` | `~/foldrun` | where the install lives (installer only) |
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 |
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 |
116
+ | `FOLDRUN_DATA_DIR` | a named volume | a host path for the data instead |
117
+
118
+ `docker-compose.yml` lists the rest (concurrency, retention, gVisor, other
119
+ model endpoints). Put your own changes in `docker-compose.override.yml`
120
+ beside it, which Compose reads too: the installer replaces
121
+ `docker-compose.yml` on an update and keeps a copy of one you edited.
122
+
123
+ ### Updating, versions, both kinds of chip
124
+
125
+ Run the installer again. It keeps `.env` (the secret key, your settings),
126
+ moves `FOLDRUN_VERSION` to the newest `stable`, pulls and restarts; your
127
+ data stays in its volume. With a version pinned, a re-run keeps it.
128
+
129
+ `stable` is the newest version built for both amd64 and arm64, so the same
130
+ command works on an Intel server and an Apple Silicon Mac. It moves forward
131
+ once a day at most; each version is tested on foldrun's own servers first.
132
+
133
+ ### Backups
134
+
135
+ `~/foldrun/.env` (the secret key: losing it signs everyone out and voids
136
+ every webhook URL) and the data volume (`foldrun_foldrun-data`, or your
137
+ `FOLDRUN_DATA_DIR`) are the whole install. Stop with `docker compose stop`;
138
+ `docker compose down -v` deletes the data volume.
139
+
140
+ ### By hand, or from source
141
+
142
+ The installer is a convenience. The same by hand:
143
+
144
+ ```sh
145
+ mkdir ~/foldrun && cd ~/foldrun
146
+ curl -fsSLO https://raw.githubusercontent.com/foldrun-io/foldrun-web/main/self-hosting/docker-compose.yml
147
+ echo "FOLDRUN_SECRET_KEY=$(openssl rand -hex 32)" > .env
148
+ echo "FOLDRUN_PUBLIC_URL=http://localhost:3900" >> .env
149
+ echo "FOLDRUN_BIND=127.0.0.1" >> .env # this machine only, as the installer does
150
+ docker compose up -d
151
+ ```
152
+
153
+ From source instead of images — the four repositories side by side, built
154
+ here (a few minutes and some gigabytes the first time):
155
+
156
+ ```sh
157
+ for r in foldrun-core foldrun-platform foldrun-web foldrun-docs; do
158
+ git clone https://github.com/foldrun-io/$r
159
+ done
160
+ cd foldrun-web/self-hosting
161
+ echo "FOLDRUN_SECRET_KEY=$(openssl rand -hex 32)" > .env
162
+ echo "FOLDRUN_PUBLIC_URL=http://localhost:3900" >> .env
163
+ echo "FOLDRUN_BIND=127.0.0.1" >> .env
164
+ docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
165
+ ```
56
166
 
57
167
  ## Your machine
58
168
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldrun",
3
- "version": "2.8.1",
3
+ "version": "2.9.1",
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.12.1"
14
+ "@foldrun/core": "^2.13.0"
15
15
  },
16
16
  "files": [
17
17
  "bin",
package/src/commands.mjs CHANGED
@@ -5650,6 +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
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")}`);
5654
5655
  console.log(` network ${networkEnforceText(d.networkEnforce, "the install")}`);
5655
5656
  }
@@ -5951,7 +5952,7 @@ async function accountCmd(positional, flags) {
5951
5952
  if (key === "telemetry" && verb === "set") {
5952
5953
  throw new Error("telemetry has its own verb — `foldrun account telemetry set <otlp-url> [--header name=value]`; `foldrun account clear telemetry` turns it off");
5953
5954
  }
5954
- const KEYS = ["timezone", "notify", "budget", "concurrency", "deploy_block", "network_enforce", "owner", "alerts", "judge", ...(verb === "clear" ? ["telemetry"] : [])];
5955
+ const KEYS = ["timezone", "notify", "budget", "concurrency", "compute", "deploy_block", "network_enforce", "owner", "alerts", "judge", ...(verb === "clear" ? ["telemetry"] : [])];
5955
5956
  if (!KEYS.includes(key)) {
5956
5957
  throw new Error(`which setting? \`foldrun account ${verb} <${KEYS.join("|")}> …\` — or <job>.<field>, e.g. governance.owner (\`foldrun jobs\` lists them)`);
5957
5958
  }
@@ -5961,6 +5962,20 @@ async function accountCmd(positional, flags) {
5961
5962
  patch = { [key]: null };
5962
5963
  } else if (key === "alerts" || key === "judge") {
5963
5964
  patch = { [key]: await mergedEntry(url, flags, key, positional.slice(2), null, view, `foldrun account set ${key}`) };
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 };
5964
5979
  } else if (key === "notify") {
5965
5980
  // `set notify email you@example.com` / `set notify url https://…`, and
5966
5981
  // --events to change what is notified on.
@@ -9612,6 +9627,29 @@ async function usageCmd(flags) {
9612
9627
  return 0;
9613
9628
  }
9614
9629
 
9630
+ /**
9631
+ * `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).
9634
+ */
9635
+ async function usageComputeCmd(flags) {
9636
+ const url = platformFor(flags, "usage compute");
9637
+ const d = await remoteCall(url, flags, "/api/usage/compute");
9638
+ if (flags.json === true) {
9639
+ console.log(JSON.stringify(d, null, 2));
9640
+ return 0;
9641
+ }
9642
+ const p = d.compute ?? {};
9643
+ const u = d.inUse ?? { vcpu: 0, memGiB: 0, steps: 0 };
9644
+ const of = (used, cap, unit) => (cap == null ? `${used} ${unit} ${c.dim("(no cap)")}` : `${used} of ${cap} ${unit}`);
9645
+ 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`);
9650
+ return 0;
9651
+ }
9652
+
9615
9653
  /** `foldrun billing <verb>` — statement, wallet, details, portal; bare, the balance. */
9616
9654
  async function billingVerbCmd(positional, flags) {
9617
9655
  const url = platformFor(flags, "billing");
@@ -11778,6 +11816,7 @@ export async function run(command, positional, flags, workspace, layout) {
11778
11816
  case "observe":
11779
11817
  return observeCmd(flags, layout);
11780
11818
  case "usage":
11819
+ if (positional[0] === "compute") return usageComputeCmd(flags);
11781
11820
  return usageCmd(flags);
11782
11821
  case "workspace":
11783
11822
  if (positional[0] === "vocabulary") return vocabularyCmd(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. 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\`). 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