Workbench

Run the factory on cloud boxes

By default the workbench factory runs every job on one static ssh box (WORKBENCH_PROVIDER=ssh). The cloud tier lets each job lease its own fresh box from your cloud account, so many jobs run in parallel and nothing shares a machine. The gate model is unchanged - the gate still runs inside the container the target repo declares (see Prepare a repository); the cloud tier only changes where the box comes from.

Turn it on

The factory reads these from the server's environment (the server runs under doppler run), and crabbox reads its own knobs from the same environment.

Hetzner (a few cents per job)

export WORKBENCH_PROVIDER=hetzner
export HCLOUD_TOKEN=...                 # a Hetzner Cloud API token (direct mode)
export WORKBENCH_BOX_CLASS=cpx41        # 8 vCPU / 16 GB / 240 GB disk (a Hetzner server type)
export CRABBOX_HETZNER_LOCATION=ash     # a location that actually has the type in stock

Pin CRABBOX_HETZNER_LOCATION - crabbox's default (fsn1, Germany) does not always have cpx41 in stock, and a leased-in-a-stockless-location request fails fast with a clean "unsupported location for server type" (no box is created, no cost) rather than provisioning anything. ash (Ashburn, US-East) and hil (Hillsboro, US-West) are the US locations; availability moves, so if a location rejects the type, check the Hetzner API GET /v1/datacenters (server_types.available) for one that has it.

AWS

export WORKBENCH_PROVIDER=aws
# AWS credentials come from the standard SDK chain (env vars, ~/.aws, instance role) -
# the factory holds no AWS secret of its own.
export CRABBOX_AWS_REGION=us-east-1  # default eu-west-1
export CRABBOX_CAPACITY_MARKET=spot  # spot (default) or on-demand
export CRABBOX_AWS_ROOT_GB=100       # root disk in GB (crabbox reads it directly)

Daytona

Daytona is a managed sandbox, not a bare VM: you lease a container from a snapshot and reach it over Daytona's ssh gateway. Size lives on the snapshot, not --class (WORKBENCH_BOX_CLASS is ignored, with a warning). It runs in either provisioning mode:

  • Install at lease time (default). Point at a base snapshot with enough resources; the factory installs docker/gh and starts the daemon (docker-in-docker, which Daytona supports) on it each run - no baking needed.
    export WORKBENCH_PROVIDER=daytona
    export CRABBOX_DAYTONA_API_KEY=...
    export CRABBOX_DAYTONA_SNAPSHOT=daytona-large   # a base snapshot sized >= 2 CPU / 4 GB
    export CRABBOX_DAYTONA_TARGET=us                # a Daytona compute region, optional
  • Pre-baked. Bake a snapshot with the tools already in it, from the shared factory-box.sh (one source of truth) via devbox/daytona-snapshot.Dockerfile, and set WORKBENCH_PREBAKED=1 so the box boots ready with no per-run install:
    daytona snapshot create factory-box --dockerfile devbox/daytona-snapshot.Dockerfile \
      --cpu 4 --memory 8 --disk 10
    export WORKBENCH_PROVIDER=daytona WORKBENCH_PREBAKED=1
    export CRABBOX_DAYTONA_API_KEY=... CRABBOX_DAYTONA_SNAPSHOT=factory-box

A default Daytona account caps a sandbox at 4 CPU / 8 GB / 10 GB disk (contact Daytona for more); docker-in-docker wants >= 2 CPU / 4 GB.

Two operator notes for daytona:

  • crabbox must be >= 0.41. Older crabbox builds over-request sandbox memory at lease time, which trips the quota on a small Daytona org before any box exists - a failure that reads as an account limit when it is really a CLI version floor.
  • Snapshots go inactive with disuse. Daytona deactivates a snapshot that has not been used for a while, and an inactive snapshot fails lease validation until it is activated again (POST /api/snapshots/{id}/activate, or the daytona CLI).

ssh stays the default: unset WORKBENCH_PROVIDER (or set it to ssh) and the factory behaves exactly as before, no lease involved.

Box size

export WORKBENCH_BOX_CLASS=c7a.2xlarge   # default: 8 vCPU / 16 GB (a raw AWS instance type)
export CRABBOX_AWS_ROOT_GB=100           # default root disk for that box

The factory leases an 8 vCPU / 16 GB box by default with a 100 GB root disk - enough to docker build and run a heavy repo's gate without OOMing or filling the disk, while staying cheap (a few cents per job on spot). crabbox's own default is beast (a 192-vCPU machine), which the factory overrides.

WORKBENCH_BOX_CLASS is passed to crabbox as --class, which on both cloud providers accepts a raw provider type name:

  • aws: a raw EC2 instance type (the named ladder - standard = c7a.8xlarge, beast = c7a.48xlarge - jumps from small (t3.small) straight to 32 vCPU, so the default pins the exact c7a.2xlarge). Disk is a separate knob (CRABBOX_AWS_ROOT_GB).
  • hetzner: a Hetzner server-type name (cpx41 = 8 vCPU / 16 GB / 240 GB). Disk is bundled with the type - there is no separate disk knob. crabbox's beast default is not a valid Hetzner type, so a missing class errors rather than making a giant box.

Raise it (c7a.4xlarge / cpx51 / a named class) for a repo whose build or tests need more memory or CPU.

Provisioning mode

By default every cloud box installs the factory's tools at lease time (factory-box.sh), but a per-run install is wasted work at high volume. Set WORKBENCH_PREBAKED=1 to boot from a pre-baked image that already carries the tools and skip the install. It applies to any provider given a matching baked image (CRABBOX_AWS_AMI on aws, a baked CRABBOX_DAYTONA_SNAPSHOT on daytona); pointed at an unprepared image it fails fast at the gate's docker check. ssh is unaffected (its static box is prepared out of band).

The lease lifecycle

  • workbench_clone leases a box (crabbox warmup) under a slug derived from the run id: wb-<hash(run_id)>. Because the slug is derived, every step of the run targets the same box, and a retried clone re-derives the same slug rather than leasing a second box.
  • workbench_clone then provisions the fresh box before the clone lands: it runs the same devbox/factory-box.sh that prepares the static ssh box (installing docker, git, gh, and the coding agent, and creating the workbench workdir), so a bare cloud image becomes a ready factory box. Provisioning is bounded (a wedged install times out) and, if it fails, the box is released so the clone retry starts on a clean box. A cloud provider image that blocks egress to get.docker.com/cli.github.com/claude.ai fails here with a legible error rather than later in the gate.
  • workbench_attempt and workbench_submit reuse that box (crabbox run --id <slug>).
  • workbench_submit releases the box (crabbox stop --id <slug>) once the PR is opened.
  • The clone output surfaces the box - box: { slug, provider, id } - so every run's box is visible in its run doc, and submit reports box_released.

Cost and safety

  • Bounded on a crash. A run that errors before submit never releases its box, but crabbox's idle-timeout (CRABBOX_IDLE_TIMEOUT, default 30m) and TTL (CRABBOX_TTL, default 90m) terminate an idle box on their own. Worst case is roughly 30 minutes of one box. Until the timeout fires, that box also holds a provider quota/concurrency slot - on a small Daytona quota enough to block the next lease - so clear stale sandboxes before retrying a failed run.
  • A capacity/credential failure is legible. If a box cannot be leased (no spot capacity, a bad token, a down region), the run reports a clean "could not lease a cloud box: ..." error and the workflow's clone retry 2 gives it a couple more tries before it drafts out - it does not crash or scatter across machines.
  • Concurrency is isolated. Each run has a distinct id, so a distinct slug, box, workdir, and compose project; one run's box release can never touch another's.

Recovering a leaked box

Every box's slug is wb-<hash(run_id)>, printed in the run's clone output. To reclaim one by hand:

crabbox stop --provider <hetzner|aws> --id <slug>      # stop one box by its slug (the wb-... value)
crabbox cleanup --provider <hetzner|aws>               # sweep expired direct-provider boxes

Not built (deliberate)

  • Per-run region/market/size overrides. Provider selection and sizing are process-env globals today (one operator, one config); a per-run override is a future add.
  • An explicit run-error finalizer. A crashed run leans on the idle-timeout floor above rather than eagerly stopping the box; a finalizer is a possible later addition.
  • A max-concurrent-lease cap. Twenty queued jobs lease twenty boxes; the blast radius is operator-driven. A CRABBOX_MAX_LEASES-style cap is a future knob.

Next

On this page