Skip to main content

rindo-provisioner — the Rindo reference provisioner (decision #90, RP3)

A controller that turns ONE pool's demand into one-job ephemeral runners inside E2B sandboxes. It reads the pool's demand, mints a registration per job it can serve, creates a sandbox from the agent-host template, revokes the sudo grant the sandbox provisioning added, starts rindo-runner inside it with executor = "process" and the registration's credential, and kills the sandbox when that runner exits. It never claims a job, never calls /mcp or a runner verb, never reads a payload, never retries a job, never listens inbound, never calls an LLM — the server stays dumb and the provisioner stays outside it.

Operator documentation lives in the Rindo ops runbook §4.13 ("Runner pools and the provisioner"). This README is the package-local quickstart.

Install (on the operator box)

uv tool install rindo-provisioner       # or: pip install rindo-provisioner
rindo-provisioner --check
# from a source checkout instead: cd agents/provisioner && uv sync && uv run rindo-provisioner --check

Python 3.11+. Two runtime dependencies: httpx (the two pool doors are plain REST) and the e2b SDK (the sandbox substrate). Apache-2.0 (see LICENSE — like rindo-runner, not part of the proprietary Software); published on PyPI.

Configure

Flags > RINDO_PROVISIONER_<KEY> env vars > a TOML file > defaults, validated before any network call (exit 2 on a problem). The two secrets have no flag:

Setting Flag Env var Config key Default
Server URL --server-url RINDO_PROVISIONER_SERVER_URL server_url —
Pool token (rndp_…) (none) RINDO_PROVISIONER_TOKEN token —
E2B API key (e2b_…) (none) E2B_API_KEY e2b_api_key —
Runtime (one per process) --runtime RINDO_PROVISIONER_RUNTIME runtime —
Template --template RINDO_PROVISIONER_TEMPLATE template —
Runner command --runner-command RINDO_PROVISIONER_RUNNER_COMMAND runner_command —
Claim-by TTL (s) --ttl-seconds RINDO_PROVISIONER_TTL_SECONDS ttl_seconds 300
Own sandboxes at once --max-sandboxes RINDO_PROVISIONER_MAX_SANDBOXES max_sandboxes 2
Env allowlist (none) RINDO_PROVISIONER_SANDBOX_ENV sandbox_env (empty)
Poll cadence (s) --poll-seconds RINDO_PROVISIONER_POLL_SECONDS poll_seconds 10
Controller id --controller-id RINDO_PROVISIONER_CONTROLLER_ID controller_id <hostname>-<runtime>
Sandbox cap (s) --max-lifetime-seconds RINDO_PROVISIONER_MAX_LIFETIME_SECONDS max_lifetime_seconds 3600 (Hobby)
Create rate (/s) --creates-per-second RINDO_PROVISIONER_CREATES_PER_SECOND creates_per_second 1 (Hobby)
Log level / JSON --log-level / --log-json …_LOG_LEVEL / …_LOG_JSON log_level / log_json INFO / off
# ~/.config/rindo-provisioner/config.toml   (chmod 600 — it holds two secrets)
server_url = "https://rindo.example.com"
token = "rndp_xxxxxxxxxxxxxxxxxxxxxxxx"      # minted on Account → Runners & pools, or POST /api/v1/account/pools/{id}/tokens
e2b_api_key = "e2b_xxxxxxxxxxxxxxxx"         # or E2B_API_KEY in the environment
runtime = "claude-code"
template = "rindo-agent-host"               # built once from the agent-host image (below)
runner_command = "env DISABLE_AUTOUPDATER=1 GH_NO_UPDATE_NOTIFIER=1 LANG=C.UTF-8 rindo-entrypoint rindo-runner --log-json --command 'claude -p --dangerously-skip-permissions {payload_file}'"
sandbox_env = ["ANTHROPIC_API_KEY"]          # names from THIS process's environment that enter a sandbox
# max_lifetime_seconds = 86400               # a Pro E2B tier
# creates_per_second = 5
  • The pool token is minted on Account → Runners & pools (a holder's own pool — the reveal prints this file beside the once-shown rndp_ with the three operator values marked: the runtime, the template, the E2B key) or with a PAT: POST /api/v1/account/pools/{id}/tokens, or POST /api/v1/admin/pools/{id}/tokens for an instance pool (API-only — ruling D4); runbook §4.13 / §8. It opens exactly two doors: the demand read and the ephemeral registration. Its revocation stops the provisioner at its next request (exit 3).
  • The web reveal prints the same shape as this file — the TOML in one well, the chmod and the two --config commands in another, each with its own Copy; its runner_command and sandbox_env are the Claude Code example above, labelled as such — another agent CLI adapts both. --check tests neither the template, the runner command nor the agent's credentials (it reads the demand and lists your sandboxes).
  • runtime: a provisioner serves ONE runtime; run a second process for another. Its spelling is the server's skills-name rule exactly — lowercase letters and digits in runs joined by single dashes (claude-code, codex), 1–64 chars.
  • controller_id must be UNIQUE per process — it is the tag a restart adopts by, and two processes sharing it would adopt and kill each other's sandboxes. The default <hostname>-<runtime> covers one process per runtime on a box; a second process for the SAME runtime on one box sets its own.
  • runner_command runs inside the sandbox as the image's rindo user. Go through rindo-entrypoint so the Codex MCP wiring happens (the image's ENTRYPOINT never runs in a sandbox). The runner reads its credential, the server URL and RINDO_RUNNER_EXECUTOR=process from the environment the provisioner constructs — never put them in the command.
  • sandbox_env is an allowlist of names from the provisioner's OWN environment; nothing crosses by default. The provisioner's credentials and namespace (E2B_API_KEY, RINDO_PROVISIONER_*) and the three handles it constructs (RINDO_RUNNER_TOKEN / _SERVER_URL / _EXECUTOR) are refused by name.
  • ttl_seconds is the registration's claim-by deadline: the sandbox must boot and poll inside it (M3 measured boot → the runner's first round trip at ~3 s; a failed create holds a booting slot for the whole TTL, so keep it short).
  • max_lifetime_seconds / creates_per_second are the E2B tier's limits (Hobby 1 h and 1/s; Pro 24 h and 5/s). A sandbox lives ttl + job_max_duration + tail seconds (tail = beat + min(beat, 30) + 60, beat = heartbeat / 2 — every term served by the demand read); when that sum exceeds the cap the provisioner launches NOTHING and logs the bound job_max_duration_seconds ≤ cap − ttl − tail — never a silent clamp.
  • E2B_DEBUG in the environment is a config error: the SDK's debug mode never kills a sandbox.

The template (a recipe, not a build script)

Build the agent-host image from runner/Dockerfile (the default agents target), push it to a private registry (it bakes in Claude Code under Anthropic's terms — never a public image), then build the E2B template once with the Template SDK (e2b template build no longer exists):

from e2b import Template, default_build_logger

template = (
    Template()
    .from_image("ghcr.io/<org>/rindo-agent-host:<tag>", username="<user>", password="<read:packages PAT>")
    .set_user("rindo")          # from_image keeps NEITHER the image's USER nor its WORKDIR (M3)
    .set_workdir("/workspace")
)
Template.build(template, "rindo-agent-host", cpu_count=2, memory_mb=2048, on_build_logs=default_build_logger())

No start command: the image's ENTRYPOINT never runs in a sandbox (PID 1 is the guest's init), which is why runner_command goes through rindo-entrypoint. The image's ENV lines do not reach a sandbox command either (from_image drops them and set_envs is build-time only) — prefix the runner command with them: env DISABLE_AUTOUPDATER=1 GH_NO_UPDATE_NOTIFIER=1 LANG=C.UTF-8 rindo-entrypoint rindo-runner --log-json --command '…', or list them in sandbox_env. The image must carry rindo-runner ≥ 1.2 (executor = "process" is a 1.2 key). The pull credential is a recipe-time secret the operator holds — it is not a provisioner key. 2 vCPU / 2 GiB is the measured shape; a smaller one is unmeasured.

What happens per job

  1. GET /api/v1/pool/demand — per runtime: queued (jobs this pool's runners could claim), budget_waiting (held by their project's window — never launched on), booting, polling, busy, plus the three facts of the lifetime formula.
  2. Launches = queued − booting − polling − own creates in flight, capped by max_sandboxes and the create rate.
  3. Per launch: POST /api/v1/pool/runners (one ephemeral registration, its rndw_ shown once) → Sandbox.create(template, timeout, metadata, on_timeout = kill) → the root bootstrap → rindo-runner started as rindo with the credential in the command's own environment (never create-time envs, never metadata — E2B echoes those) → when the runner exits, kill().
  4. A failed create or bootstrap leaves its registration to die at its TTL (the server retires it; nothing claimed). A job is never retried by the provisioner.

The bootstrap. E2B's provisioning adds an account user with passwordless sudo to every template. Before the runner starts — and before the credential enters the sandbox — the provisioner runs ONE fixed root command that removes user from the sudo / admin / wheel groups and every direct grant in /etc/sudoers and /etc/sudoers.d/*, validates with visudo -cq, and proves sudo -n true is refused for BOTH user and rindo. It returns 0 only when both probes are refused; any other outcome (a non-zero exit, a timeout, a transport error) kills the sandbox and the runner never starts. Three consecutive bootstrap failures exit 2 — a template fault is an operator fault. The isolation claim stays the memo's P13: the runner and its one job share the sandbox and its uid; the rndw_ can claim no second job and dies with this one.

Restarts. The provisioner tags every sandbox (rindo_server, rindo_pool, rindo_runner, rindo_controller). On start it adopts its own tagged sandboxes by each one's OWN deadline (a lowered ttl_seconds never shortens a claimed job), re-attaches to the runner command, and kills a sandbox whose runner is gone. It never touches another controller's. A kill the provider refuses keeps the sandbox counted against max_sandboxes (a "zombie") until the provider stops listing it or a retried kill lands — never a silent drop. SIGTERM / SIGINT stop the loop and leave live sandboxes to their deadlines; SIGQUIT drains — nothing more is launched and the process exits once its own sandboxes have ended.

--check

ONE demand read + ONE sandbox listing, never a registration: prints the pool, the runtime's counts, the three facts, the computed lifetime and whether it fits the cap, and the number of own sandboxes.

Exit codes

Code Meaning
0 clean stop, drained, or a successful --check
1 server or substrate unreachable during --check (the daemon backs off instead)
2 configuration error — incl. --token / --e2b-api-key on the command line (refused by name, the value never echoed), E2B_DEBUG set, a secret-holding config file readable by others, three consecutive bootstrap failures
3 the pool token refused (revoked, or its pool retired)

What to alert on

A booting count that stays high across polls (sandboxes that never claim — the TTL, the template, or the server URL as seen from the sandbox); "sandbox bootstrap failed" (the template); "exceeds max_lifetime_seconds" (the tier cap vs the instance's job_max_duration); exit 3 (rotate the pool token: mint the new one, start the provisioner on it, revoke the old — runbook §8).

Develop

uv sync
uv run ruff check . && uv run ruff format --check .
uv run pytest -q          # offline: a FakeSubstrate + a MockTransport; the bootstrap
                          # constant runs under a real bash with stubbed tools

Metadata

Release files for rindo-provisioner 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rindo-provisioner 0.1.0
File Size Uploaded
rindo_provisioner-0.1.0.tar.gz 65.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rindo-provisioner 0.1.0
File Interpreter ABI Platform
rindo_provisioner-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 102.4 kB

Release files / rindo_provisioner-0.1.0.tar.gz

Download URL rindo_provisioner-0.1.0.tar.gz
Size 65.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c14d428d75e62cca202adf2165e87a2cbe1d9f11a7a02c6cbef1ac2520ed3ed3
BLAKE2b-256 checksum
How to use checksums
0ba1876bbf46443ae823f708c71ffbc393948bf7b4a1e9697d0a23e66dccb0b8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / rindo_provisioner-0.1.0-py3-none-any.whl

Download URL rindo_provisioner-0.1.0-py3-none-any.whl
Size 37.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
23be32bbd657062f0c65fb394fc7cc5ce6f2aa43cecf26859eb602d89a0cc9cd
BLAKE2b-256 checksum
How to use checksums
ce3aee6739d6763d913c164213f317bd573a7d5321f5dc0baa080c49bfd958a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.6 {"installer":{"name":"uv","version":"0.12.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page