foldrun 2.8.0 → 2.9.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
@@ -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,74 @@ 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
+
374
+ Pick `small` for an agent that mostly waits on an API, `heavy` for one that
375
+ renders a browser or holds real data. The step's `resources:` line reports CPU
376
+ busy time, **throttled** time (how long the cap held it back), and peak memory.
377
+ A step that is throttled for much of its run wants a bigger class; one whose
378
+ peak memory is a fraction of the class's figure can take a smaller one.
379
+
380
+ ### `resources:` — your own numbers
381
+
382
+ When no class fits, write the numbers. Each key replaces that one figure of
383
+ the class; the rest stay the class's:
384
+
385
+ ```yaml
386
+ size: large
387
+ resources:
388
+ cpu: 1 # cores, 0.5 to 8, in halves
389
+ memory: 6Gi # 256Mi to 16Gi, in steps of 256Mi
390
+ disk: 30Gi # whole GiB, 1Gi to 40Gi
391
+ ```
392
+
393
+ Examples:
394
+
395
+ - One core and a lot of memory, for an agent that loads a big CSV.
396
+ - A lot of disk, for one that renders video.
397
+
398
+ The figures are hard limits and what the step is billed at, exactly as for a
399
+ class. The step's header on the run page shows what it held ("1 vCPU ·
400
+ 6 GiB"), with the class and your `resources:` in its tooltip.
401
+
402
+ The keys are bounded and stepped the way sandbox platforms sell them, so
403
+ steps pack onto a machine predictably. `foldrun check` reports a value
404
+ outside them (`config/invalid-value`); at run time that one key is dropped,
405
+ the class's figure is used, and the step's log says so. Sizes need a unit:
406
+ `4Gi` or `512Mi`, never a bare `4`, and not `4G` (Kubernetes reads `G` as
407
+ decimal, Docker as binary).
408
+
409
+ `resources:` may also be written inside `guardrail:` ([four jobs](four-jobs)).
410
+ When a retry moves a step up a class after it ran out of memory or disk, each
411
+ of your figures is raised to the bigger class's, so the retry never runs
412
+ smaller than the class it moved to.
413
+
414
+ Your account's plan may also cap what one step can hold, and how much all of
415
+ an account's running steps hold together
416
+ ([budgets and billing](budgets-and-billing)).
357
417
 
358
418
  ```yaml
359
419
  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 |
@@ -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,101 @@ 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 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:
43
38
 
44
39
  ```sh
45
40
  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
41
+ foldrun login --url http://localhost:3900
42
+ foldrun deploy # from your account folder
48
43
  ```
49
44
 
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.
45
+ To read the script before it runs (anything piped into a shell deserves it):
46
+ `curl -fsSL https://foldrun.io/install.sh -o install.sh`, read it, then
47
+ `sh install.sh`. Its source is `self-hosting/install.sh` in
48
+ [foldrun-web](https://github.com/foldrun-io/foldrun-web/tree/main/self-hosting).
49
+
50
+ The platform runs in one container — the dashboard, the API, the scheduler
51
+ and the queue — and every agent step runs in a throwaway container beside
52
+ it, started through the Docker socket. The images are
53
+ `ghcr.io/foldrun-io/platform` and `ghcr.io/foldrun-io/runner`, public, built
54
+ from these repositories.
55
+
56
+ ### Settings
57
+
58
+ Give them to the installer as environment variables
59
+ (`curl -fsSL https://foldrun.io/install.sh | FOLDRUN_BIND=0.0.0.0 sh`) or
60
+ edit `~/foldrun/.env` and run `docker compose up -d` there.
61
+
62
+ | variable | default | what |
63
+ |---|---|---|
64
+ | `FOLDRUN_PUBLIC_URL` | `http://localhost:3900` | the address people open. CLI sign-in, approve links, password resets and shares are made with it |
65
+ | `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 |
66
+ | `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` |
67
+ | `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
+ | `FOLDRUN_DIR` | `~/foldrun` | where the install lives (installer only) |
69
+ | `FOLDRUN_DOCKER_SOCK` | `/var/run/docker.sock` | the Docker socket; rootless Docker is found by the installer |
70
+ | `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
+ | `FOLDRUN_DATA_DIR` | a named volume | a host path for the data instead |
72
+
73
+ `docker-compose.yml` lists the rest (concurrency, retention, gVisor, other
74
+ model endpoints). Put your own changes in `docker-compose.override.yml`
75
+ beside it, which Compose reads too: the installer replaces
76
+ `docker-compose.yml` on an update and keeps a copy of one you edited.
77
+
78
+ ### Updating, versions, both kinds of chip
79
+
80
+ Run the installer again. It keeps `.env` (the secret key, your settings),
81
+ moves `FOLDRUN_VERSION` to the newest `stable`, pulls and restarts; your
82
+ data stays in its volume. With a version pinned, a re-run keeps it.
83
+
84
+ `stable` is the newest version built for both amd64 and arm64, so the same
85
+ command works on an Intel server and an Apple Silicon Mac. It moves forward
86
+ once a day at most; each version is tested on foldrun's own servers first.
87
+
88
+ ### Backups
89
+
90
+ `~/foldrun/.env` (the secret key: losing it signs everyone out and voids
91
+ every webhook URL) and the data volume (`foldrun_foldrun-data`, or your
92
+ `FOLDRUN_DATA_DIR`) are the whole install. Stop with `docker compose stop`;
93
+ `docker compose down -v` deletes the data volume.
94
+
95
+ ### By hand, or from source
96
+
97
+ The installer is a convenience. The same by hand:
98
+
99
+ ```sh
100
+ mkdir ~/foldrun && cd ~/foldrun
101
+ curl -fsSLO https://raw.githubusercontent.com/foldrun-io/foldrun-web/main/self-hosting/docker-compose.yml
102
+ echo "FOLDRUN_SECRET_KEY=$(openssl rand -hex 32)" > .env
103
+ echo "FOLDRUN_PUBLIC_URL=http://localhost:3900" >> .env
104
+ echo "FOLDRUN_BIND=127.0.0.1" >> .env # this machine only, as the installer does
105
+ docker compose up -d
106
+ ```
107
+
108
+ From source instead of images — the four repositories side by side, built
109
+ here (a few minutes and some gigabytes the first time):
110
+
111
+ ```sh
112
+ for r in foldrun-core foldrun-platform foldrun-web foldrun-docs; do
113
+ git clone https://github.com/foldrun-io/$r
114
+ done
115
+ cd foldrun-web/self-hosting
116
+ echo "FOLDRUN_SECRET_KEY=$(openssl rand -hex 32)" > .env
117
+ echo "FOLDRUN_PUBLIC_URL=http://localhost:3900" >> .env
118
+ echo "FOLDRUN_BIND=127.0.0.1" >> .env
119
+ docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
120
+ ```
56
121
 
57
122
  ## Your machine
58
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldrun",
3
- "version": "2.8.0",
3
+ "version": "2.9.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.12.0"
14
+ "@foldrun/core": "^2.13.0"
15
15
  },
16
16
  "files": [
17
17
  "bin",
@@ -47,7 +47,7 @@
47
47
  "devDependencies": {
48
48
  "@types/node": "^26.6.4",
49
49
  "gray-matter": "^4.0.3",
50
- "typescript": "^6.0.3"
50
+ "typescript": "^7.0.2"
51
51
  },
52
52
  "overrides": {
53
53
  "zod": "4.6.7"
package/src/commands.mjs CHANGED
@@ -883,6 +883,7 @@ async function scanCore() {
883
883
  }
884
884
 
885
885
  // The scanner module, once scanCore has loaded it — categoryOf and SARIF read it.
886
+ /** @type {any} */
886
887
  let scanRules = null;
887
888
 
888
889
  const SECURITY_CATEGORIES = new Set(["secret", "security", "content", "supply-chain"]);
@@ -1027,6 +1028,7 @@ function narrowCheck(findings, root, opts, label, accountRoot = root) {
1027
1028
  }
1028
1029
  let baselined = 0;
1029
1030
  if (opts.baseline && fs.existsSync(file)) {
1031
+ /** @type {any} */
1030
1032
  let b = null;
1031
1033
  try {
1032
1034
  b = JSON.parse(fs.readFileSync(file, "utf8"));
@@ -1540,6 +1542,7 @@ async function checkHooks(workspace, agents, libraryRoot, platform, note) {
1540
1542
  missing("account AGENTS.md", accountNames);
1541
1543
  missing("AGENTS.md", workspaceNames);
1542
1544
 
1545
+ /** @type {any} */
1543
1546
  let resolveSearch = null;
1544
1547
  try {
1545
1548
  ({ resolveSearch } = /** @type {any} */ (await import("@foldrun/core/providers")));
@@ -1812,6 +1815,7 @@ async function probeCmd(modelArg) {
1812
1815
  // provider gets the same translator a run would, on loopback, for the
1813
1816
  // length of the probe — so what passes here passes there.
1814
1817
  let env = { ...process.env };
1818
+ /** @type {any} */
1815
1819
  let translator = null;
1816
1820
  // No AGENTS.md, or no provider block, means Anthropic direct — like a run.
1817
1821
  // Anything else that goes wrong here is said, not swallowed: a provider
@@ -2446,6 +2450,7 @@ async function versionCmd(flags) {
2446
2450
  const url = remoteUrl(flags);
2447
2451
  /** @type {Record<string, any> | null} */
2448
2452
  let platform = null;
2453
+ /** @type {string | null} */
2449
2454
  let unreachable = null;
2450
2455
  if (url) {
2451
2456
  try {
@@ -3671,6 +3676,7 @@ async function connect(positional, flags) {
3671
3676
  // secret, nothing typed. The redirect is the platform's own callback, which
3672
3677
  // rewrites the same secret at the same scope; we wait for it to land.
3673
3678
  if (url && !flags["client-id"] && !env("OAUTH_CLIENT_ID") && flags["new-client"] !== true) {
3679
+ /** @type {any} */
3674
3680
  let started = null;
3675
3681
  try {
3676
3682
  started = await remoteCall(url, flags, "/api/oauth/reconnect", {
@@ -4492,6 +4498,7 @@ async function deployAccount(layout, flags, only) {
4492
4498
 
4493
4499
  /** "<profile> (<email or key label>)" — who a deploy is about to act as,
4494
4500
  * asked of the platform once per command. */
4501
+ /** @type {string | null} */
4495
4502
  let identityCache = null;
4496
4503
  async function deployIdentity(url, flags) {
4497
4504
  if (identityCache) return identityCache;
@@ -4889,6 +4896,7 @@ async function pullCmd(layout, flags, only) {
4889
4896
 
4890
4897
  let written = 0;
4891
4898
  for (const f of incoming) {
4899
+ /** @type {string | null} */
4892
4900
  let before = null;
4893
4901
  try {
4894
4902
  before = fs.readFileSync(f.file, "utf8");
@@ -4968,6 +4976,7 @@ async function workspacesCmd(positional, flags, layout) {
4968
4976
 
4969
4977
  const here = layout.workspaces;
4970
4978
  let there = [];
4979
+ /** @type {string | null} */
4971
4980
  let reach = null;
4972
4981
  if (url) {
4973
4982
  try {
@@ -5641,6 +5650,7 @@ function printDefaults(d) {
5641
5650
  console.log(` notify ${notify}`);
5642
5651
  console.log(` budget ${d.budget ? `$${d.budget.usd} per ${d.budget.period}` : c.dim("no cap")}`);
5643
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)")}`);
5644
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")}`);
5645
5655
  console.log(` network ${networkEnforceText(d.networkEnforce, "the install")}`);
5646
5656
  }
@@ -5942,7 +5952,7 @@ async function accountCmd(positional, flags) {
5942
5952
  if (key === "telemetry" && verb === "set") {
5943
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");
5944
5954
  }
5945
- 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"] : [])];
5946
5956
  if (!KEYS.includes(key)) {
5947
5957
  throw new Error(`which setting? \`foldrun account ${verb} <${KEYS.join("|")}> …\` — or <job>.<field>, e.g. governance.owner (\`foldrun jobs\` lists them)`);
5948
5958
  }
@@ -5952,6 +5962,20 @@ async function accountCmd(positional, flags) {
5952
5962
  patch = { [key]: null };
5953
5963
  } else if (key === "alerts" || key === "judge") {
5954
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 };
5955
5979
  } else if (key === "notify") {
5956
5980
  // `set notify email you@example.com` / `set notify url https://…`, and
5957
5981
  // --events to change what is notified on.
@@ -8064,6 +8088,7 @@ function writeIfUnchanged(desk, rel, before, next) {
8064
8088
 
8065
8089
  /** Throw, as writeIfUnchanged does, when a desk file no longer reads `before`. */
8066
8090
  function assertUnchanged(desk, rel, before) {
8091
+ /** @type {string | null} */
8067
8092
  let now = null;
8068
8093
  try {
8069
8094
  now = fs.readFileSync(path.join(desk.dir, rel), "utf8");
@@ -9602,6 +9627,29 @@ async function usageCmd(flags) {
9602
9627
  return 0;
9603
9628
  }
9604
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
+
9605
9653
  /** `foldrun billing <verb>` — statement, wallet, details, portal; bare, the balance. */
9606
9654
  async function billingVerbCmd(positional, flags) {
9607
9655
  const url = platformFor(flags, "billing");
@@ -11768,6 +11816,7 @@ export async function run(command, positional, flags, workspace, layout) {
11768
11816
  case "observe":
11769
11817
  return observeCmd(flags, layout);
11770
11818
  case "usage":
11819
+ if (positional[0] === "compute") return usageComputeCmd(flags);
11771
11820
  return usageCmd(flags);
11772
11821
  case "workspace":
11773
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