The open Range where koboi ride out & work — self-hosted, scale-to-zero, suspend/resume.
A per-session, on-demand deployment runner for koboi-agent: one isolated Mount (Cloudflare Container) per agent session, spun up when there's work, dismounted to a Saddlebag (R2 snapshot) when idle (≈ $0), and remounted on resume. No standby container. Your brain, your infra.
koboi-agent is "self-hostable AI agents you can actually leave running." Today that means
keeping a server container warm 24/7 — even when a session sits for hours in pending_approval
waiting for a human. koboi-range kills that idle bill: sessions that aren't actively working
are snapshotted to object storage and pay ~nothing, then resume exactly where they left off
(koboi's durable steps journal makes the gap survivable).
This is the same deployment pattern as Devin Outposts (per-session container, queue-driven scale-to-zero, suspend/resume via FS snapshot) — but the agent brain is yours (open Python, BYO-LLM), not a closed vendor cloud.
Wave-0 status — proof scaffold, not production. Wired to koboi-agent 0.19.2's
POST /v1/sessions/{id}/suspend(atomicity-independentsqlite3backup). Deps install andtsc --noEmitpass clean against@cloudflare/sandbox@0.12.4. Remaining gaps are operational (CF account / KV / R2 / secrets). See Honest caveats.
Every technical concept maps to one cowboy term — koboi is the Indonesian for cowboy, and the Range is its home territory.
| Range term | Means |
|---|---|
| Range | the platform — your sovereign execution infra |
| Outrider | the edge coordinator (Cloudflare Worker + cron) that dispatches per session |
| Mount | one per-session Cloudflare Container — a keep-alive; the Outrider starts/stops koboi serve inside it |
| Saddlebag | the /workspace snapshot (koboi_memory.db + steps journal + audit git) → createBackup/restoreBackup to R2 |
| Ride | boot the Mount, restore+swap its Saddlebag if resuming, then start koboi serve + wait ready |
| off the Range | suspended / scale-to-zero (≈ $0) — dismount |
| Remount | resume — fresh Mount + restoreBackup + swap the consistent snapshot in + restart koboi serve |
| Retire | terminate — drop Mount + Saddlebag |
controller browser ─┐
├─▶ OUTRIDER (Cloudflare Worker, edge, ~$0 idle)
overnight cron ─────┘ │ • route per-session traffic to the right Mount
│ • /lifecycle/* control API + /lifecycle/observe webhook
│ • cron 1/min = the "Range heartbeat"
▼
RANGE_KV (session registry: status + Saddlebag handle)
│
│ getSandbox(env.Sandbox, sessionId) [Sandbox SDK]
▼
┌──────────────────────────────────┐ ◀── restoreBackup() on remount
│ MOUNT (CF Container, instance=sid)│ createBackup() on dismount
│ koboi serve (single-session) │
│ /workspace ◀── the Saddlebag root ──▶ │
│ ├ koboi_memory.db (WAL sqlite) │
│ ├ steps journal (durable) │
│ └ audit git (sandbox.git_init) │
│ mcp: erp_mcp_server.py (stdio) │
│ tools: finance_ext.* (incl. │
│ post_journal_entry DESTR.) │
└──────────────┬───────────────────┘
│ (real ERP only)
▼ CF Tunnel / Hyperdrive → internal services
R2 bucket "range-saddlebags" ← squashfs Saddlebags (TTL/lifecycle backstop)
Three pieces, three homes:
| Piece | Lives in | Stack |
|---|---|---|
| Mount image + use-case config | mount/ |
Dockerfile (Python + koboi [api]) |
| Outrider coordinator | outrider/ |
TypeScript / Cloudflare Workers (@cloudflare/sandbox) |
| The koboi brain itself | koboi-agent (consumed as a PyPI image) | Python |
The pattern earns its keep on sessions that wait for a human — like Ledgerline's controller approving a journal entry. The wait is no longer billed:
t0 controller: "reconcile INV-8842 vs PO-4471"
Outrider: POST /lifecycle/ride/<sid>
→ Mount boots (keep-alive); restoreBackup(<saddlebag>) if resuming, else fresh
→ swapSnapshot (resume only): mv koboi_memory.db.<sid>.suspend.db → koboi_memory.db
→ startProcess("koboi serve") + wait /healthz 200 (the DB opens eagerly at boot)
t1 koboi act-loop: fetch_invoice → fetch_po → three_way_match [SAFE reads, no gate]
koboi calls post_journal_entry (DESTRUCTIVE) → pending_approval → awaiting_human → IDLE
t2 cron (1/min): awaiting_human + idle > 60s → DISMOUNT:
→ POST /v1/sessions/<sid>/suspend → koboi writes a consistent snapshot
(sqlite3 Online Backup API — atomicity-independent of createBackup)
→ createBackup({dir:"/workspace"}) → R2 Saddlebag (snapshot file + workdir + audit)
→ pkill koboi serve → Mount scales to zero 💤 ~$0 while the controller reviews
t3 controller clicks Approve (hours later) → status → resuming → cron REMOUNTS:
→ fresh Mount + restoreBackup → swapSnapshot → startProcess("koboi serve") + wait ready
→ koboi resume_on_startup rehydrates the interrupted turn; post_journal_entry completes
t4 run terminal → cron sees done → RETIRE: stop serve + drop Saddlebag
Why the keep-alive Mount?
koboi serveopens the shared SQLite DB eagerly increate_app(JobStore/OwnershipStore, pre-lifespan) andresume_on_startupreads it at lifespan startup — before the first request. So the resume-side snapshot swap must happen beforekoboi servestarts (else split-brain: sidecars hold the old file whileresume_on_startupran on stale rows). The Outrider therefore owns thekoboi servelifecycle (start after restore+swap; stop before restore). koboi'sstepsjournal makes the t2→t3 gap survivable regardless.
koboi-range/
├── mount/ # the Mount image
│ ├── Dockerfile # koboi [api] + git + /workspace Saddlebag root, single-session
│ ├── configs/finance.yaml # Ledgerline config (adapted from koboi-use-cases)
│ └── usecase/finance-ext/ # vendored use-case code (tools + stdio ERP MCP server)
├── outrider/ # the Outrider (edge coordinator)
│ ├── wrangler.jsonc # containers + Durable Object + R2 + KV + cron bindings
│ ├── package.json / tsconfig.json
│ └── src/
│ ├── index.ts # Worker: routes + scheduled cron (Range heartbeat)
│ └── lib/
│ ├── sandbox.ts # ride/dismount/remount/retire via @cloudflare/sandbox
│ └── registry.ts # RANGE_KV session registry
├── demo/roundtrip.sh # drives the t0→t4 lifecycle against a deployed Outrider
└── README.md
Prereqs: a Cloudflare account with Workers + Containers + R2, wrangler logged in, and an
OpenAI key (or your provider) for the koboi Mount.
cd outrider
npm install # pins real dep versions; resolves the TS types
# 1. create the backing stores
npx wrangler kv namespace create RANGE_KV # paste the id into wrangler.jsonc
npx wrangler r2 bucket create range-saddlebags
# 2. set the koboi/LLM secret on the Mount (rides the container env)
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put OPENAI_MODEL # e.g. gpt-4o-mini
# 3. fill wrangler.jsonc placeholders: KV id, CLOUDFLARE_ACCOUNT_ID
# 4. deploy the Outrider + Mount image + cron
npx wrangler deploy
# 5. prove a ride survives suspend/resume
../demo/roundtrip.sh <YOUR_WORKER_URL> demo-session-1The Outrider has a local test suite that runs without a Cloudflare account or deploy —
the per-session Mount (Cloudflare Container) is mocked, while the registry + the cron heartbeat
run against real ephemeral KV (Miniflare). A real ride (live koboi serve in a live Container)
still needs a Paid CF account + deploy (see Deploy).
cd outrider
npm ci
npm run typecheck # tsc --noEmit over src/ (the ship gate)
npm test # vitest: 17 tests across registry / routing / lifecycle / scheduledSee CONTRIBUTING.md for conventions and docs/ROADMAP.md for the Wave-0 → Wave-1b TODO list.
- Not deployable blind. Needs
npm install, a CF account, KV + R2 created, secrets set, and thewrangler.jsoncplaceholders filled. The repo is a head start, not a one-click ship. @cloudflare/sandboxAPI verified against v0.12.4.d.ts(2026-07-23):getSandbox, theSandboxDO-class re-export, and thecreateBackup({dir,name,ttl})→DirectoryBackup→restoreBackup(handle)flow all typecheck clean. One hard constraint caught & fixed:BackupOptions.dirmust be under/workspace·/home·/tmp·/var/tmp·/app(not/data) — hence the Saddlebag root is/workspace. Per-instance container teardown (sb.stop()/destroy) is still a Wave-1b TODO (today: TTL auto-GC + idle scale-to-zero).- Consistency is via koboi 0.19.2
/suspend, not WAL quiesce. On dismount the Outrider callsPOST /v1/sessions/{id}/suspend, which writes a consistent snapshot via the sqlite3 Online Backup API (atomicity-independent — safe even while other connections write). The Outrider thencreateBackups/workspace(capturing that file + workdir + audit). On resume it restores + swaps the snapshot into place before startingkoboi serve. No raw WAL-trio file-copy. - Control plane is all SDK RPC (no Worker→container HTTP). Readiness uses
proc.waitForPort(8000, {path:"/healthz"});/suspend+ session create/verify run a one-shot HTTP call from inside the Mount (localhost:8000) viasb.exec. This is why it works on.workers.dev(noexposePort/tunnel/custom-domain). Live-token chat streaming (the data plane) is wired over a public Mount URL:proxyToSandbox(request, env)gatesfetch()and proxies<port>-<sid>-<token>.<PUBLIC_DOMAIN>subdomains straight to the per-session Mount'skoboi serve(unbuffered body → SSE/v1/chat/streamflows token-by-token);exposePort(8000, {hostname, token})is re-activated on everyride/remountso the preview URL is stable and survives suspend/resume. RequiresPUBLIC_DOMAIN+ wildcard DNS*.<PUBLIC_DOMAIN>→ the Worker (a deploy step).PUBLIC_DOMAINmust be a dedicated zone apex you own (e.g.koboi-range.dev), never a shared zone's apex — the streaming wildcard route claims the whole zone, so*.<shared-zone>/*shadows every sibling project on it (we hit this: a*.lab-sandbox.dev/*route made an unrelated sibling worker's/healthzanswer as the Outrider). A dedicated root domain also keeps preview URLs one level deep, covered by free Universal SSL; deeper subdomains need Advanced Certificate Manager or a self-uploaded Let's Encrypt wildcard, and delegating one as its own zone is Enterprise-only. Live token-by-token delivery still wants acurl -Nsmoke check. Quick tunnels (*.trycloudflare.com) are unsuitable: they buffer SSE and the URL churns every remount. pending_approvalobservation is via a webhook receiver (/lifecycle/observe/:sid). Wire the Mount'sjobs.webhooks/handover.webhooksto POST there. (Polling the Mount's job status from the cron is the alternative.)- Native CF container disk-suspend + native snapshots are "coming soon." Today
suspend/resume =
createBackup/restoreBackup(FUSE overlay). Same API going forward. - Fits heavy per-session workloads (reconciliation, contract review, research) — not high-fanout multi-tenant chat, where a warm multi-tenant server is cheaper.
- koboi-agent — the brain (consumed as
koboi-agent[api]==0.19.2in the Mount image). ThePOST /v1/sessions/{id}/suspendendpoint +SQLiteMemory.consistent_backup()this repo consumes shipped in 0.19.1 (PR #98); GET /v1/sessions/{id} surviving a serve restart (the suspend/resume fix) shipped in 0.19.2 (PR #103). - koboi-use-cases — the sector apps (finance-reconciliation is the demo use case vendored here). Sibling repo, same "consume koboi" pattern.
MIT, matching koboi-agent.