Overview
Remote devboxes let internal root workspace members offload expensive local work to self-hosted runner machines. A runner connects outbound totuturuuu.com,
receives queued work, executes it on its host, streams logs, stores artifacts,
and releases the lease automatically for one-off work.
The current ttr box agent executes claimed commands as child processes on its
host. Its command policy and environment filtering are operational controls,
not an isolation boundary for untrusted learner code. Do not route Learn coding
submissions to ordinary Devbox runs. Judge needs a separate sandbox executor
and readiness checks before its capability can be enabled.
V1 is intentionally self-hosted. It does not provision cloud machines. Any
runner host should have Node.js, Bun, Docker, and Git available.
Quick Start
ttr box setup is the default bootstrap for a runner host. It first checks
whether the current directory is already a Tuturuuu platform checkout. If it is,
setup reuses that checkout. Otherwise it can clone or reuse
https://github.com/tutur3u/platform.git, install dependencies with
bun install --frozen-lockfile, start local Supabase with bun sb:start,
verify supabase status -o json, and write local Supabase connection values
into ignored apps/*/.env.local files. Secret values are redacted from human
and JSON output.
Pass --dir <path> for a specific checkout. If the target exists but is not a
valid Tuturuuu checkout, interactive setup asks whether it should clone into a
nested tuturuuu directory. In non-interactive shells, pass
--clone-into <path> explicitly:
--yes only when the host should install detected missing prerequisites
automatically. Runner registration and service installation are separate
choices.
To register the machine and install a boot-starting runner service in one
non-interactive command after login:
ttr box connect --service --runner-name "$(hostname)-devbox" instead.
This registers the machine, writes its runner token to a mode-0600 file, and
installs the same boot-starting service. The CLI prints a direct Infrastructure
dashboard link for that runner. The dashboard shows the heartbeat’s CLI version
and machine identity so an administrator can verify the connection before
enabling workloads.
The setup command stores the runner token in a restricted local env file and
installs a system-level runner service on Linux systemd or macOS launchd. The
service starts after reboot and restarts automatically if the runner exits.
Runner heartbeats report a small observability snapshot, including ttr, Bun,
Node, Docker, Git, OS, CPU, RAM, load average, and uptime. Root workspace admins
can inspect those fields from Infrastructure > Devboxes.
Devbox agent poll and heartbeat traffic is blocked by default at the platform
API boundary. Set TUTURUUU_DEVBOX_AGENT_API_ENABLED=true on the web app before
starting or repairing runners. When the flag is absent, agent poll and heartbeat
requests return 403 before runner tokens are authenticated or runner state is
updated.
Cloudflare control plane
ttr box agent start uses the existing web API until
TUTURUUU_DEVBOX_CONTROL_URL is set on the runner host. Point this variable
at the deployed tuturuuu-devbox-control Worker to send heartbeats, job claims,
and an authenticated wake connection through Cloudflare. The agent still sends
job events and completion to the web API. It polls the Worker every 30 seconds
when idle, even with a wake connection, so a missed signal cannot strand work.
The Worker checks the runner token, token revocation, runner status, and root
workspace membership before each heartbeat or claim. Disabling heartbeats on
a runner continues to reject heartbeat requests. Worker traffic requires TLS.
The production workflow installs Worker secrets from the repository’s
PRODUCTION_SUPABASE_URL, PRODUCTION_SUPABASE_SECRET_KEY, and
DEVBOX_CONTROL_INTERNAL_TOKEN secrets. It uses
COLAB_CLOUDFLARE_API_TOKEN for Cloudflare deployment. Set
TUTURUUU_DEVBOX_CONTROL_URL and TUTURUUU_DEVBOX_CONTROL_TOKEN on web and
Learn to wake an assigned runner immediately after enqueuing work. Use the
same internal token as the Worker secret; never put it in the runner’s env file.
Missing or failed wake calls do not fail enqueue, and the fallback poll catches
the job. Verify /health, then a real runner heartbeat, claim, completion,
revocation rejection, and Learn submission before moving additional runners.
The old web API remains available until all runners are migrated.
Without a Worker URL, idle claims back off from 5 to 10, 20, and then 30 seconds.
A claimed job or completion resets that delay; heartbeats remain independent at
20 seconds to preserve online detection and leases. Worker wake messages still
trigger immediate claims, with a 30-second reconciliation poll for missed wakes.
If a host completed ttr box setup but does not appear in Infrastructure >
Devboxes, confirm it was registered as a runner. Plain setup prepares the
checkout, dependencies, local Supabase, and ignored env files only; it does not
create a private.devbox_runners row. Run setup with --agent --service, or
register and start the agent manually, then confirm ttr whoami on that host is
pointing at the same production origin and root workspace user.
If the host appears as registered but shows no heartbeat, inspect the installed
service on that host:
devbox-runner.env file. If
the journal shows a missing runner token or stale wrapper, upgrade the CLI and
repair the service. Repair reuses the existing token file; it does not register
a new runner token or create another runner row.
bun i -g tuturuuu for the selected runner, waits
for completion, and returns the remote command logs and exit code. After a
successful CLI update, the agent exits; a runner installed with --service
restarts under systemd or launchd and reports its new CLI version on the next
heartbeat. A manually started agent must be restarted by its operator. The run
completion and the subsequent heartbeat are separate verification steps.
Platform-owned fleet controls
Root members can use the existing Infrastructure authorization boundary throughclient.devboxes.fleet.snapshot() or the CLI:
--apply performs sequential,
allowlisted maintenance, rechecks fresh heartbeats and unfinished work before
each operation, stops on failure, and waits for a subsequent healthy heartbeat.
Upgrades additionally verify the advertised CLI version against the registry
release. Offline, revoked, busy, or truncated snapshots are skipped. Snapshot
limits mean this is a conservative operator convenience, not an atomic host-wide
reservation; coordinate a maintenance window when service users share an install.
Capabilities, feature switches, and resource limits are available as structured
JSON for tooling extensions. These commands do not enable arbitrary shell access
or change runner workload permissions. Custom SDK deployments use their own
origin rather than forwarding credentials to hosted Infrastructure.
Protecting hosts with existing critical files
Use--execution-mode judge-only with box connect, box setup, or box repair. Supplying Judge images during registration defaults to Judge-only;
general development runners retain trusted execution. Manual Judge-only setup
restricts accepted jobs but does not receive protected-service host isolation;
use the protected Linux service setup for hosts with critical files. --control-url persists
the HTTPS Cloudflare Worker URL during registration. Protected service setup
requires Linux systemd, a pre-existing non-login service user with its private
home under /var/lib, a private service-owned token directory, and checkout and
CLI outside personal homes. The dedicated root-owned Unix Docker socket must be
mode 0660 and restricted to that user’s group; the user must not belong to the general
docker group. Preflight verifies the daemon defaults to runsc with cgroup
limits before registering a credential. It does not create OS users, move host
files, or reconfigure a shared Docker daemon automatically.
0700; group and other access are rejected. Protected systemd units hide personal homes, make the host
filesystem read-only except runner directories, remove capabilities, restrict
devices, namespaces and kernel access, and set a private umask. Repair preserves
an existing Judge-only policy and refuses to weaken it; --dry-run performs the
same preflight before service writes. Setup persists the dedicated service user
for subsequent repair. Older private token files need --service-user once;
repair then retains it without rotating the runner token. The wrapper prefers the service user’s
global ttr executable so upgrades affect the running service. Before starting
a protected agent, it requires box agent policy to succeed on that executable;
an older CLI lacking policy support fails closed rather than launching. Launchd lacks
these Linux protections; use a dedicated Linux VM for protected Judge setup on
macOS. Local policy enforcement requires the CLI release containing this change.
Judge-only servers must keep Run, Build, Preview, and Tunnel disabled in
Infrastructure. Set TUTURUUU_DEVBOX_EXECUTION_MODE=judge-only in the private
runner environment after installing the release containing this local policy.
It rejects host commands, environment files, and environment overrides on
maintenance jobs even if a remote workload switch is accidentally enabled.
Unknown modes fail closed. The exact CLI-update and supervised-restart commands
remain root-member-authorized maintenance operations.
Judge submissions use digest-pinned images, gVisor runsc, an unprivileged
container user, no network, a read-only root filesystem, no host bind mounts,
dropped capabilities, no privilege escalation, and CPU/memory/PID/time limits.
Never expose the Docker socket to submissions. The runner and dedicated Docker
daemon remain trusted infrastructure: access to a rootful Docker socket is a
privileged control plane, even when its default runtime is gVisor. Keep the
service user out of the general Docker group, use a dedicated Unix socket, hide
personal home directories, and do not use this host for arbitrary trusted devbox
commands. Stronger isolation for additional workloads requires a separate VM or
host; a shell-command denylist is not a security boundary.
Operator-owned automatic upgrades
For a fleet whose service users do not have CLI user sessions, schedule upgrades from an authenticated operator machine with an explicit runner allowlist:~/.tuturuuu/devbox-upgrades. It uses the existing CLI session and
never copies credentials into its plist. launchctl print gui/$(id -u)/com.tuturuuu.devbox-cli-upgrades shows scheduler status. A full
snapshot, an active lease, stale heartbeat, or unfinished job prevents upgrade;
an unsuccessful run or version heartbeat stops the remaining operations.
Keep runner self-upgrades disabled when this scheduler owns maintenance.
Ensure the service’s executable points at the same global installation that
bun i -g tuturuuu updates for its OS user. A successful install into a different
path does not update a service launching an older private copy. After changing a
systemd path, drain work, reload the unit, restart sequentially, and verify each
runner’s heartbeat and CLI version.
Automatic CLI upgrades
New or repaired managed services defaultTUTURUUU_DEVBOX_AUTO_UPGRADE=false.
Enable TUTURUUU_DEVBOX_AUTO_UPGRADE=true in the private runner environment only
after selecting an upgrade owner and maintenance policy. Existing services need one repair after the
release containing this capability is installed:
tuturuuu release pinned to its exact version, verifies
--version, and exits for its service manager to restart. It pauses new claims
during installation and keeps heartbeating. A private host lock serializes
installs from runners sharing an OS user home directory, and persisted check state
prevents restart retry loops. Cached install state is scoped to the verified
CLI command, and a failed cached-version verification resumes its registry check. Other agents using that shared install restart
after verifying the installed version. Lock contention schedules another attempt
after 30 seconds, at the next idle claim, rather than delaying a stale process
for a full day. Registry checks remain limited by the shared daily state. Each agent drains its own jobs; for hosts
with different OS service users or unrelated CLI consumers, coordinate a
host maintenance window or opt out instead of assuming this lock covers them.
Installer subprocesses receive host path/runtime variables, never the runner
token. An unverified install attempts to restore the previous release. A failed
rollback stops the agent with a repair error; inspect service logs and repair
the host installation. Locks record process ownership and identity; dead owners are reclaimed under a
separate reclamation guard, while live installers are never evicted by age. An
unknown legacy lock requires operator inspection. Reclaim guards also record
process ownership; an interrupted guard reports a repair-required error rather
than being deleted automatically or retrying silently. Never remove a lock held by a running installer.
Set TUTURUUU_DEVBOX_AUTO_UPGRADE=false in devbox-runner.env to opt out.
Manually started and source-checkout agents do not upgrade automatically unless
explicitly configured, and source .ts entry points never mutate the global CLI.
Infrastructure’s Update CLI remains the bootstrap for existing runners;
confirm both its successful run and the subsequent version heartbeat. Source
changes take effect only after the normal tested CLI release is published.
Regression coverage: devbox-auto-upgrade.test.ts covers stable version
validation, pinned installation, shared-host locking, restart throttling,
verification and rollback. devbox-agent.test.ts covers claim backoff,
independent heartbeats, concurrency, draining and supervised restart;
devbox-setup.test.ts verifies the service wrapper preserves the explicit opt-in.
Infrastructure > Devboxes lets root administrators enable or disable command,
build, preview, and tunnel workloads per runner. New runners default to those
four workload types enabled. Disabling a type stops new matching jobs from being
claimed on that runner; it does not interrupt a running job or act as a
security permission for root workspace members who can submit generic commands.
The dashboard keeps switches unavailable until the
20260928130000_devbox_runner_features.sql migration is applied. Its Update CLI
action queues an update for an online runner, then the agent restarts as above.
The Restart agent action and ttr box restart queue a typed maintenance job
that exits the agent after successful acknowledgement; the installed service
manager starts it again. These actions restart the agent process, not the
operating system.
Judge defaults off. Enabling it requires a recent heartbeat reporting a
verified sandbox runtime and at least one pinned, cached language image.
Existing Devbox host commands remain separate from Judge submissions.
The same dashboard stores a runner’s Judge capacity budget. Defaults reserve
at most 50% of host CPU and 25% of host memory, with one sandbox and one
instance at a time. Each sandbox defaults to 1 GB, 30 seconds, and 64
processes. Administrators can adjust these limits per machine. These settings
are enforced on each Judge container by Docker cgroups, with a conservative
free-memory check before each case. The platform claim function caps concurrent
Judge runs on a runner at the lower of its sandbox and instance maxima. Each
container receives a share of the host CPU and memory budget divided by that
ceiling. A managed agent can dispatch independent Judge jobs concurrently within
the configured process-wide sandbox budget. These settings do
not limit the current trusted host commands, builds, previews, or tunnels.
Judge setup
The Judge executor requires a Docker engine with therunsc gVisor runtime
registered, working CPU/memory/process cgroup limits, and locally cached OCI
images pinned to SHA-256 digests. The CLI fails readiness when these are absent
or the Docker engine is rootless with resource enforcement not verified. gVisor
currently requires a Linux host or Linux VM. Do not enable Judge on a machine
that has only the default Docker runtime.
Configure the languages you want with a JSON map of language names to pinned
image references. Supported adapters are Python, JavaScript, TypeScript, C,
C++, Java, Rust, Go, Ruby, and PHP. The chosen image must contain that
language’s runtime or compiler plus a POSIX shell and base64 for compiled
languages. Pull each digest on the host before connecting. Then run:
Recover a Lima Judge VM after disk errors
On macOS, a full host disk can leave a Lima guest’s ext4 root filesystem read-only. If writes fail withRead-only file system or the VM stops before
SSH is ready, restore host free space first. Keep the VM disk and runner token
files. Check df -h on the host and remove only known rebuildable caches or
clean, merged worktrees.
If the guest remains read-only, stop the VM before checking its disk. On an
APFS host, make a copy-on-write backup, then attach the raw disk without
mounting it:
diskutil output (for example,
/dev/disk4s1). Install the bottled e2fsprogs package if needed. Run the
read-only check first, then repair that partition with the block device
path, not the raw /dev/rdisk* path:
ttr box run creates an auto lease, waits for an authenticated
runner to claim the job, streams recorded logs after completion, and releases
the lease after one-off work. Use --keep for repeated sync/run work:
Build, Serve, And Tunnel
Usettr box build for common build workloads without spelling the full remote
command every time:
ttr box serve when the remote devbox should build an app, start the
server, and keep the run alive. It defaults to apps/web on port 7803, stores
the preview port on the run, and returns immediately unless --wait is passed:
ttr box build, ttr box serve, and ttr box tunnel do not set a remote
command timeout by default. Pass --timeout <duration> only when the run should
be killed automatically.
To expose a served app through a Cloudflare tunnel, keep the token in a local
environment variable and pass the variable name to the devbox. The queued
command references $CLOUDFLARED_TOKEN; the raw token is delivered only as
run-scoped env and is redacted from logs.
ttr box tunnel runs a dockerized cloudflare/cloudflared container with host
networking on the runner. The command policy blocks inline cloudflared --token ... arguments, so operators should use --cloudflared-token-env or
another local environment variable indirection rather than pasting raw tokens
into commands.
Container Boundary
The current agent starts brokered commands directly on the host. The command policy blocks several destructive command forms, but is not a sandbox. These machines must be operated as trusted infrastructure. Docker-backed commands create their own containers only when the requested command does so. Allowed remote workflows include:bun check,bun test, and package-local Bun commandsbun test:e2eand Docker-backed Playwright workflowsbun sb:start,bun sb:reset,bun sb:up, andbun sb:typegen
sudo, and host-destructive filesystem
operations.
Env And Secrets
Remote env is explicit. Local.env* files are never synced automatically.
The setup command only prepares ignored app env files inside the runner
checkout so local Supabase-backed apps and E2E workflows can use that runner’s
own Supabase stack.
DEVBOX_DATABASE_URL on the operator machine, and
queue the app devbox with --database-url-env DEVBOX_DATABASE_URL. This lets a
database-heavy runner and a Next.js runner share work while the current machine
only brokers commands and opens previews.
Runner scheduling and hosted environments
Agents keep heartbeats on a 20-second cadence while dispatching independent jobs concurrently, with at most eight active jobs. Judge cases share a process-wide sandbox budget and always use fresh isolated containers. Each case reserves1 / parallelLimit of the host budget, so mixed jobs cannot each claim the
whole memory or CPU allocation. Eligible smaller reservations can pass a waiting
full-budget case. Infrastructure failures cancel queued cases and wait for
already admitted commands and container cleanup before reporting failure.
Regression coverage lives in devbox-sandbox-slots.test.ts and
devbox-judge-case-batch.test.ts. Maintenance and CLI
restart jobs drain active work before changing the agent. An execution failure
stops new dispatch and drains already active work before the agent exits. Heartbeats
continue during that drain, and local completions wake idle claim polling. The
parent scheduling client omits Playground capabilities until both heartbeat
contracts support them; devbox-agent-capabilities.test.ts exercises its emitted
body against both current handlers. Judge readiness probes are cached for ten minutes and refreshed in the background; execution still fails
closed if required isolation or language images are unavailable.
Platform-managed playground workers opt in through operator configuration. They
require gVisor and pinned language image digests, run unprivileged without host
credentials, and disable network access by default. Project environments retain
dependencies while warm. A configured TUTURUUU_PLAYGROUND_POOL_ID dedicates the
agent process to Playground: Judge readiness is false and Judge execution fails
before Docker admission, so warm environments cannot double the Judge host budget.
Idle/max-age eviction waits for an active project run and holds the pool lock;
near-expiry containers are replaced before admitting another bounded run. Failed
starts remove their exact owned name, retaining quarantined capacity when Docker
cannot confirm removal. Environments expire after fifteen idle minutes and have a two-hour
maximum lifetime. Environment count is bounded by the runner’s resource limits.
A failed container removal retains its capacity reservation until a successful
removal or healthy Docker inventory confirms the container is absent. Failed
startup orphan cleanup blocks new environments and is retried on the next
admission. Creation and stop removal share the same pool lock. Same-project runs
serialize synchronization, execution and saves; stop invalidates queued runs
without waiting for a running command. Stopping waits for admitted creation, with a fifteen-second
wait bound; a timeout reports an unconfirmed stop and retains capacity ownership.
It does not wait for the arbitrary project command to finish. Python helpers use
the pinned image PATH, including images that install Python under /usr/local/bin.
A failed background save emits a fixed diagnostic and makes the run report
incomplete persistence rather than silently claiming success. These lifecycle
rules are covered by devbox-playground-sandbox.test.ts.
File inventories and changed bytes are exported separately; credentials and
runner metadata never enter project files. File/directory conflicts are checked
against every path ancestor, and preview paths reject traversal before URL
normalization. Export transport permits the worst-case JSON escaping of a valid
two-MiB text project; unsupported paths or oversized files report the offending
file instead of failing with an opaque schema error. The schema and Python
export contracts cover these rules. The platform must provide its signed
file-save callback before enabling the hosted playground workload. The scheduling
primitives alone reject the reserved hosted Playground envelope without running a
host command; its HTTP adapter ships together with the canonical file-save API.
The typed-adapter regression in devbox-runner-judge.test.ts verifies hosted execution uses the sandbox and canonical authenticated callback, while malformed envelopes never fall through to host execution.
Use focused SDK agent, Judge and sandbox-slot tests for scheduling changes. A
passing mock test does not establish real sandbox startup latency; measure cold
and warm execution on a configured gVisor runner before advertising timings.
Do not fall back to ordinary Docker isolation when gVisor is missing.
Cache Policy
Runners use named cache volumes for Bun installs, Turbo, Playwright browsers, Supabase Docker state, package manager cache, and optionalnode_modules.
Cache keys include the repo fingerprint, lockfile hash, runtime image digest,
platform, command profile, Bun/Node versions, and cache schema version.
Cleanup runs on runner startup, after runs, and during heartbeat maintenance.
The runner evicts incompatible caches first, including legacy Bun install caches
when the Bun version, lockfile hash, package profile, or cache schema changes.
It then enforces cache budgets with least-recently-used eviction while keeping a
small set of recent compatible caches to avoid thrashing.
Operators can inspect and prune cache metadata:
Access
Remote devbox API routes require a Tuturuuu CLI/app session and root workspace membership withworkspace_members.type = 'MEMBER'. Guests and non-root
workspace users cannot create runs, leases, previews, or runner tokens.
Local Executable Verification
Use an isolated CLI config when testing localapps/web so the production CLI
session is not overwritten:
private.devbox_runs.status should end
as succeeded with exit_code = 0.
Production Readiness
The production API needs migration20260603171600_create_private_devboxes.sql before any ttr box command can
create leases or runs. If the CLI prints a schema-cache error such as
private.devbox_leases not being found, first confirm the production migration
workflow ran for the deployed SHA. If the table exists but PostgREST still
returns the schema-cache error, refresh the schema cache from Supabase SQL
Editor:
--all with explicit runner IDs. Maintenance
is deferred when a runner does not report its hostname. Fleet upgrades honor
--timeout; a timed-out run requests cancellation and reports whether that
request succeeded. Verify its run and lease before retrying or touching another
runner, since a stop request does not prove immediate process termination.
Runtime Acceptance Diagnostics
Judge capacity discovery runs a strict four-seconddocker info metadata probe
before allocating a sandbox. A failure reports a fixed timeout, spawn
(not found, permission denied, or other), numeric exit, signal, or missing-exit
classification. These diagnostics omit Docker hosts, command arguments, raw
output, and provider error messages. A timeout is not proof that the engine is
absent: inspect the owning CI job and runner lifecycle before repairing or
retrying. Successful discovery still requires registered gVisor, enforced
cgroups, and reported CPU/memory capacity. Mocked process tests verify the
classification and failure boundary; they do not verify a runner’s Docker engine.
The Lettin persistence browser regression separately reports project-navigation
confirmation and the notebook-button action, retaining their existing deadlines
and assertions. Phase failures emit an allowlisted error name and a fixed message
summary rather than authenticated URLs or response bodies. A runner shutdown
without a final assertion report leaves the underlying cause unproven. Preserve
allocated fixtures until successful final cleanup or independently verified
resource absence, and use the next exact-source CI result for runtime evidence.