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, orPOST /api/v1/admin/pools/{id}/tokensfor 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
chmodand the two--configcommands in another, each with its own Copy; itsrunner_commandandsandbox_envare the Claude Code example above, labelled as such — another agent CLI adapts both.--checktests 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_idmust 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_commandruns inside the sandbox as the image'srindouser. Go throughrindo-entrypointso the Codex MCP wiring happens (the image's ENTRYPOINT never runs in a sandbox). The runner reads its credential, the server URL andRINDO_RUNNER_EXECUTOR=processfrom the environment the provisioner constructs — never put them in the command.sandbox_envis 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_secondsis 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 abootingslot for the whole TTL, so keep it short).max_lifetime_seconds/creates_per_secondare the E2B tier's limits (Hobby 1 h and 1/s; Pro 24 h and 5/s). A sandbox livesttl + job_max_duration + tailseconds (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 boundjob_max_duration_seconds ≤ cap − ttl − tail— never a silent clamp.E2B_DEBUGin 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
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.- Launches =
queued − booting − polling − own creates in flight, capped bymax_sandboxesand the create rate. - Per launch:
POST /api/v1/pool/runners(one ephemeral registration, itsrndw_shown once) →Sandbox.create(template, timeout, metadata, on_timeout = kill)→ the root bootstrap →rindo-runnerstarted asrindowith the credential in the command's own environment (never create-time envs, never metadata — E2B echoes those) → when the runner exits,kill(). - 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)
| File | Size | Uploaded | |
|---|---|---|---|
| rindo_provisioner-0.1.0.tar.gz | 65.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|