Install

One control plane, then as many machines as you like. Both halves come out of the same build.

Build

There are no published binaries yet, so everything is built from source. The workspace produces three programs: the CLI, the control plane and the runner daemon.

Anywhere with a Rust toolchain
git clone https://github.com/milanofthe/falllow
cd falllow
cargo build --release
# target/release/{falllow, falllow-server, falllow-runnerd}

The control plane

It needs Postgres and a directory to keep workspaces and artifacts in. Migrations run on start, so there is no separate step. Bind it to localhost and put a reverse proxy with TLS in front, runners and the CLI both talk to it over the network, and the tokens they carry should not.

On the VPS
export DATABASE_URL=postgres://falllow:…@localhost/falllow
export FALLLOW_BIND=127.0.0.1:8080
export FALLLOW_DATA_DIR=/var/lib/falllow

falllow-server serve

Who may sign in

The manager signs people in with a link in a mailbox. There is no password and no user account: the address is proved by the auth service and traded for an ordinary API token bound to that browser.

Who gets a link is the auth app's invite list, and that is the whole of it. The control plane keeps no second list, because two doors in front of one room means adding every colleague twice and being locked out by whichever one you forgot. It does mean the invite list is load-bearing: an auth app left open to all comers hands out tokens to anyone who can read their own mail.

What each person's token carries

A first sign-in writes a row and gives it every scope, since the invite list has already vouched for them. Narrow an individual where that is too much, before or after they have ever signed in; the change revokes the token they are holding, so it takes effect now rather than at their next visit.

On the control plane
falllow-server operator list
falllow-server operator scopes colleague@example.com --scopes jobs:read,jobs:submit
falllow-server operator revoke someone@example.com

operator revoke kills somebody's live tokens immediately. It does not shut the door behind them: signing in again is the invite list's business.

Tokens for scripts and CI are still made by hand, and so is the first one on a control plane with no auth app configured, where the manager asks for a token instead.

A credential for a script
falllow-server token create --kind api --name ci --scopes all
# flw_a_…

Logs go to stderr and the token to stdout, so falllow-server token create … > token.txt captures the token and nothing else.

Scopes

ScopeAllows
jobs:submitSubmit, cancel and retry jobs.
jobs:readList jobs, read logs and artifacts.
devices:readSee machines and their load.
devices:adminEnroll and revoke machines, connect repositories.
tokens:adminIssue and revoke tokens.

An agent usually needs jobs:submit and jobs:read and nothing more.

Adding a machine

Enrollment tokens are single use: one token redeems into one device-bound credential, and the token is spent. Create it in the manager under Tokens, or on the control plane.

On the control plane
falllow-server token create --kind enrollment --name nuc-01
# flw_e_…

Then redeem it on the machine. The runner reports its real inventory, cores, memory, GPUs, OS, and takes a budget that leaves the owner room to work. Anything you do not specify falls back to a share of the machine rather than all of it.

On the machine
falllow-runnerd enroll \
  --server https://app.falllow.com \
  --token flw_e_… \
  --name nuc-01 \
  --label cuda-12 \
  --max-cores 8 --max-memory-mb 32768
FlagMeaning
--nameDisplay name in the manager. Defaults to the hostname.
--labelA capability this machine offers, e.g. cuda-12. Repeatable, and jobs can require it.
--max-coresThreads the pool may reserve.
--max-memory-mbMemory the pool may reserve.
--max-cache-gbDisk the shared caches may use.
--idle-required-secsOnly accept work after this much user idle time. For a machine someone is sitting at.
--forceReplace an existing enrollment on this machine.

What a job can reach

Every job runs in a container. Not as an option, and not as advice: the control plane fills in an image for anything submitted without one, and requires a machine that can deliver it. A job is somebody else's code on somebody else's machine, and the container is the only thing standing between the two.

The job sees one directory, its own workspace, mounted at /workspace and made the working directory. It gets no host cache paths and never sees the state directory, so it cannot read the device credential and act as that machine against the control plane.

This means a machine needs a container runtime to take work at all. The runner reports the tier it can actually deliver, and the scheduler skips machines that cannot, rather than sending them work they would have to run unconfined.

The default image is a plain base, so a job says what it needs: --image python:3.12 is one flag, and a command missing from the image fails with a message that says exactly that. A deployment can change the default with FALLLOW_DEFAULT_IMAGE.

Start it on boot

Linux

The unit carries Delegate=yes, which hands the service its own cgroup subtree. Without it the runner cannot enforce a job's thread and memory reservation at all, the limits silently become suggestions.

systemd
sudo falllow-runnerd install-service
# writes /etc/systemd/system/falllow-runner.service and enables it

# or, without root:
falllow-runnerd install-service --user

Windows

A real service, registered through the service control manager. Jobs are held in a job object with a hard CPU rate cap and a memory cap, and the object is configured to kill everything in it when the runner lets go, so a job cannot outlive its own tree.

The default account is LocalSystem, which gives jobs full machine privileges. Point --account at a limited account unless you mean that.

Windows service
# elevated PowerShell
falllow-runnerd.exe install-service --account "$env:COMPUTERNAME\falllow"
# without --account the service runs as LocalSystem

Point the CLI at it

Store an API token once per machine you submit from. The command verifies the credential immediately rather than letting the next one fail.

On your workstation
falllow login --server https://app.falllow.com --token flw_a_…
falllow devices

Caches

Each runner keeps four cache layers outside the ephemeral job workspace, given up in this order when disk runs short: compiler output, workspace files, downloaded packages, then resolved environments. The order is the design, a compiler cache is cheap to rebuild, a resolved environment is the most expensive thing here to recreate.

They are shared by every job on the machine. That is what makes a warm run fast, and it is also a real boundary: a job can write into the shared cache and affect later, unrelated jobs. The trust model today is you and people you trust. falllow-runnerd cache status shows what each layer holds and cache prune --layer packages clears one.