From 09517862fd457a09ac028354d6c423302857bea6 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 31 Jul 2026 17:31:21 -0700 Subject: [PATCH 1/2] [eric] runner: the app is live on its own isolated network, and its wall-clock cap moves into the image --- openswarm-runner/Dockerfile | 4 ++++ openswarm-runner/README.md | 39 +++++++++++++++++++++++++++++++++++-- openswarm-runner/fly.toml | 20 +++++++++---------- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/openswarm-runner/Dockerfile b/openswarm-runner/Dockerfile index d8d9e024..ab07e9c4 100644 --- a/openswarm-runner/Dockerfile +++ b/openswarm-runner/Dockerfile @@ -77,10 +77,14 @@ RUN set -eux; \ USER runner WORKDIR /app +# RUNNER_MAX_RUN_SECONDS lives in the IMAGE, not in fly.toml: machines are created one +# per run through the Machines API, which ignores fly.toml's [env], so a cap defined +# there would silently not apply to the only machines that ever run a workflow. ENV HOME=/home/runner \ PYTHONPATH=/app \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ + RUNNER_MAX_RUN_SECONDS=1800 \ OPENSWARM_HEADLESS=1 \ OPENSWARM_PACKAGED=1 \ OPENSWARM_DATA_ROOT=/data/openswarm \ diff --git a/openswarm-runner/README.md b/openswarm-runner/README.md index cb99f1d6..7ea2bfb1 100644 --- a/openswarm-runner/README.md +++ b/openswarm-runner/README.md @@ -64,5 +64,40 @@ PYTHONPATH=.:openswarm-runner backend/.venv/bin/python3 -m pytest openswarm-runn ## Deploy -Not deployed. `fly.toml` is written but never applied; read its header first, the app -has to be created onto its own isolated private network by hand before any deploy. +The app exists and is created onto its own isolated private network. Read `fly.toml`'s +header before touching it; the network is fixed at create time and cannot be changed +by a redeploy. + +```bash +# from the REPO ROOT, the image needs backend/ in its build context +fly deploy . --app openswarm-runner --config openswarm-runner/fly.toml \ + --dockerfile openswarm-runner/Dockerfile --image-label latest --ha=false +``` + +`--image-label latest` is load-bearing: the control plane creates machines from the +fixed tag `registry.fly.io/openswarm-runner:latest`, so a redeploy without it ships an +image nothing will ever boot. Re-verify the isolation after any deploy, do not assume +it survived: + +```bash +fly machine run registry.fly.io/openswarm-runner:latest -a openswarm-runner \ + --entrypoint /bin/sleep --restart no --vm-memory 512 --vm-cpus 1 600 +fly ssh console -a openswarm-runner --machine -C "getent hosts openswarm-cloud.internal" +# must print nothing and exit 2. Then destroy the probe machine. +``` + +The deploy leaves one stopped template machine with no run spec. That is expected; it +exits 2 immediately and `[[restart]] policy = 'never'` stops it looping. + +## How a run gets here + +`openswarm-cloud` creates one machine per due workflow through the Fly Machines API +(`workflows/dispatch.ts`). It never uses `fly deploy` for a run, so this app's env is +whatever the IMAGE carries plus `OPENSWARM_RUN_SPEC_FILE`; `fly.toml`'s settings do not +reach a per-run machine. Control-plane side that means: + +| env on openswarm-cloud | why | +| --- | --- | +| `FLY_API_TOKEN` | app-scoped deploy token for `openswarm-runner`, nothing wider | +| `RUN_CALLBACK_BASE_URL` | where the runner reports; **no default**, so a staging control plane can never point its machines at prod | +| `RUNNER_APP` / `RUNNER_IMAGE` / `RUNNER_REGION` | optional overrides of `openswarm-runner` / the `:latest` tag / `iad` | diff --git a/openswarm-runner/fly.toml b/openswarm-runner/fly.toml index ff2ed454..1f7ff741 100644 --- a/openswarm-runner/fly.toml +++ b/openswarm-runner/fly.toml @@ -30,17 +30,15 @@ kill_timeout = '30s' [build] dockerfile = 'Dockerfile' -[env] - # Hard wall-clock cap, enforced twice inside the container: the poll loop stops the - # run at this mark, and an independent thread kills the process 90s later. A run - # spec asking for more is clamped down to this, never up. - RUNNER_MAX_RUN_SECONDS = '1800' - OPENSWARM_HEADLESS = '1' - OPENSWARM_PACKAGED = '1' - OPENSWARM_DATA_ROOT = '/data/openswarm' - OPENSWARM_HOST = '127.0.0.1' - OPENSWARM_PORT = '8324' - DATA_DIR = '/data/9router' +# No [env] block on purpose. Per-run machines are created through the Machines API, +# which does not read this file, so anything set here would apply to the deploy's +# template machine and to nothing that actually runs a workflow. Every runtime value, +# including the RUNNER_MAX_RUN_SECONDS wall-clock cap, is baked into the image instead. + +# The template machine this deploy creates has no run spec, so it exits 2 immediately. +# Without this it would crash-loop on Fly's default on-failure policy and bill forever. +[[restart]] + policy = 'never' # No [[mounts]]: a run's state is garbage the moment it ends, and an ephemeral rootfs # means one run cannot leave a credential lying around for the next tenant to find. From 6974c342f52d7482fb2d94600a13ec4fa501405a Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 31 Jul 2026 18:04:02 -0700 Subject: [PATCH 2/2] [eric] runner: document the deploy, the control-plane env contract and the three wall-clock walls --- openswarm-runner/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openswarm-runner/README.md b/openswarm-runner/README.md index 7ea2bfb1..45cabb96 100644 --- a/openswarm-runner/README.md +++ b/openswarm-runner/README.md @@ -101,3 +101,11 @@ reach a per-run machine. Control-plane side that means: | `FLY_API_TOKEN` | app-scoped deploy token for `openswarm-runner`, nothing wider | | `RUN_CALLBACK_BASE_URL` | where the runner reports; **no default**, so a staging control plane can never point its machines at prod | | `RUNNER_APP` / `RUNNER_IMAGE` / `RUNNER_REGION` | optional overrides of `openswarm-runner` / the `:latest` tag / `iad` | +| `CLOUD_RUNS_GLOBAL_CAP` | machines this whole service will run at once, all accounts together (default 50) | +| `CLOUD_RUNS_TICK_BUDGET` | machines one 60s tick will start (default 20); the rest keep their slot for the next tick | + +A run gets three walls on its wall clock, and only the third survives a wedged VM: +the runner stops its own poll loop at `max_run_seconds`, an independent thread inside +it kills the process 90s later, and the control plane destroys the machine outright +5 minutes past that. Verified live: a machine with a sleeping entrypoint that never +reported was destroyed by the control plane and its run row closed as failed.