Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
c36f01d
The ledger's single-writer lock is held for the daemon's lifetime, no…
Annactswell Sep 11, 2026
23b9857
Server exports its auth, keyed queue, ledger lock and shutdown as sub…
Annactswell Sep 13, 2026
6bd61ab
lookupSandbox: a node answers "do you hold this sandbox?" by name or …
Annactswell Sep 13, 2026
bbfb3a8
A node checks in with its gateway every 15 seconds: readings, build a…
Annactswell Sep 13, 2026
bc0e761
packages/gateway: the fleet's one door — nodes check in, new names ar…
Annactswell Sep 13, 2026
7b560ef
A sandbox placed and destroyed inside one check-in interval no longer…
Annactswell Sep 13, 2026
2fc3ea9
e2e: the gateway exam — two daemons behind a real gateway, a third th…
Annactswell Sep 13, 2026
4c697dd
The gateway's systemd unit, its row in the README, and the shared cha…
Annactswell Sep 13, 2026
f2df652
main.ts: import order, as biome sorts it
Annactswell Sep 13, 2026
fceecef
Gateway review: a wake is not a placement, an invalid name is refused…
Annactswell Sep 13, 2026
6701d59
A node whose gateway is on another machine must set DORMICE_NODE_ENDP…
Annactswell Sep 13, 2026
042b265
lookupSandbox takes the name's slot in one step: locks.run already an…
Annactswell Sep 13, 2026
1c4f1a2
A node endpoint is an origin, and a redirected check-in is reported i…
Annactswell Sep 13, 2026
9180737
forwardStream sends nothing for a client that already left
Annactswell Sep 13, 2026
a9b735b
A creator confirms its cached node before waking there; removeNode an…
Annactswell Sep 13, 2026
d3cc130
forwardStream flushes the node's head as soon as it arrives
Annactswell Sep 13, 2026
4bb011a
The check-in's first CPU reading is an honest null, not a percentage …
Annactswell Sep 13, 2026
00f6e45
The lookup gives up in two seconds on a black-holed node too, and say…
Annactswell Sep 14, 2026
bafb602
A creator whose client left while it waited for the slot asks no node
Annactswell Sep 14, 2026
42f9bd6
install.sh builds the gateway with the daemon and restarts a hand-ins…
Annactswell Sep 14, 2026
fb058e8
A check-in refusal reaches the log whole
Annactswell Sep 14, 2026
4996ab2
The slot comment in the native face tells the truth about the daemon
Annactswell Sep 14, 2026
7b91476
The name cache is bounded, least recently used first
Annactswell Sep 14, 2026
e179090
A 409 for two node ids at one endpoint says they are one daemon, and …
Annactswell Sep 14, 2026
18024fc
The check-in log says why in the transport's word, and speaks again w…
Annactswell Sep 14, 2026
63dc74f
A node whose gateway is on another machine must state its own DORMICE…
Annactswell Sep 14, 2026
aa362bd
A node the restarted gateway has not heard from yet cannot be removed…
Annactswell Sep 14, 2026
f5aa90b
The activity ring is gone: lifecycle moves are one structured log lin…
Annactswell Sep 14, 2026
82b8cbe
The sandbox count cap is gone: a ledger row is not a resource
Annactswell Sep 14, 2026
3338349
The gateway grows its configuration tables: fleet settings with a ver…
Annactswell Sep 14, 2026
ee195db
The gateway gets three gates, mints the fleet's API keys and hosts th…
Annactswell Sep 14, 2026
b8e3614
Managed swap leaves the fleet-wide settings: it is one machine's knob
Annactswell Sep 14, 2026
edf03de
The gateway answers the configuration verbs: settings, templates, the…
Annactswell Sep 14, 2026
c536b30
The node takes its configuration from the gateway: the check-in is th…
Annactswell Sep 14, 2026
d830de2
The console's dev proxy and settings hints follow the configuration t…
Annactswell Sep 14, 2026
cf0da88
install.sh installs the gateway beside the daemon: one token in two e…
Annactswell Sep 14, 2026
e41c805
The docs describe two doors: the gateway holds the fleet's configurat…
Annactswell Sep 14, 2026
a2c6ddc
A node waiting for its first bundle beats the watchdog and says it is…
Annactswell Sep 14, 2026
5d29c0c
install.sh's closing hint drops the backticks shellcheck reads as an …
Annactswell Sep 14, 2026
baaaaad
Every client example knocks at the gateway's door; the list caveat na…
Annactswell Sep 14, 2026
ae6f90b
The watchdog's beat is a parameter of the one wait that may beat it; …
Annactswell Sep 14, 2026
2f0fa83
The sandbox port proxy has a face at the gateway: a sandbox host is f…
Annactswell Sep 14, 2026
387be3c
The browser-direct file form's preflight is answered at the door, an …
Annactswell Sep 14, 2026
81100f8
The bare signed-URL form is routed at the gateway: a node reads a sig…
Annactswell Sep 14, 2026
13ffb23
Merge remote-tracking branch 'origin/main' into annactswell/add-clust…
Annactswell Sep 14, 2026
93ce07a
A node still waiting for its first configuration is not dialled; the …
Annactswell Sep 14, 2026
179f92c
The fleet's observation wire: lists say which nodes they could not in…
Annactswell Sep 14, 2026
b17ff96
The fleet-wide lists are merged at the gateway, the E2B list pages ac…
Annactswell Sep 14, 2026
62e101d
The fleet's state history is the gateway's: one sample per check-in, …
Annactswell Sep 14, 2026
4b63ac5
The console reads the fleet at the gateway: the overview from the fle…
Annactswell Sep 14, 2026
edb52e5
The fleet exam reads a partial list from the door when a node dies, a…
Annactswell Sep 14, 2026
b061b39
The docs say which door answers the lists and the host reading, and d…
Annactswell Sep 14, 2026
14675ee
install.sh's closing note no longer sends the sandbox list to the dae…
Annactswell Sep 14, 2026
95dedc1
The gateway's way of asking a node is ask.ts, askEach takes one reque…
Annactswell Sep 14, 2026
6c71ece
The fleet's state history is sampled on the gateway's own clock, one …
Annactswell Sep 14, 2026
fd74396
Every interval knob stops at a day, dor sandbox ls exits 1 for a part…
Annactswell Sep 14, 2026
6a37f96
The reclaim timeout is capped like the intervals, and the gateway's s…
Annactswell Sep 14, 2026
5890ea0
A node's last check-in lives on its row, and the gateway's four start…
Annactswell Sep 15, 2026
5c794b8
Neither process logs a request that succeeded; one that ended in an e…
Annactswell Sep 15, 2026
bc99760
The base image is a fleet setting, and a node pulls an image it lacks…
Annactswell Sep 15, 2026
ff2ff51
The upgrade is the fleet's: the gateway's machine first, then every n…
Annactswell Sep 15, 2026
1cb64d1
A single machine's ledger is imported into the gateway once, before t…
Annactswell Sep 15, 2026
6591b03
The node's ledger drops the three tables that moved to the gateway, n…
Annactswell Sep 15, 2026
3be1ab0
install.sh installs two roles, runs the fleet registry, backs up befo…
Annactswell Sep 15, 2026
03003d9
e2e: the base image re-pointed at the gateway reaches the nodes' copi…
Annactswell Sep 15, 2026
2566db4
Docs: adding a node, the fleet upgrade, the base image as a setting, …
Annactswell Sep 15, 2026
4b451e4
A node newer than the gateway reads ahead, never behind: not told, it…
Annactswell Sep 15, 2026
79c3a72
install.sh: a failed import removes the half-made gateway database, t…
Annactswell Sep 15, 2026
24d583b
A check-in that reads current or ahead spends the operator's pending …
Annactswell Sep 15, 2026
58a9cfc
removeNode forgets the node's pending re-tell; --node-id and --node-e…
Annactswell Sep 15, 2026
9537557
The operator's hand on a stuck node puts it back in line instead of t…
Annactswell Sep 15, 2026
69d96cb
applyUpgrade on a node refuses nodeId instead of upgrading whichever …
Annactswell Sep 15, 2026
adda883
pulseFileGrowth delivers nothing after stop(): a stat already in flig…
Annactswell Sep 15, 2026
ff0f372
A node joins through the gateway machine's Caddy on :80, not the gate…
Annactswell Sep 15, 2026
1b7d073
install.sh builds native modules from the Node headers it shipped, an…
Annactswell Sep 15, 2026
fd81d5c
install.sh fetches Docker's install script with retries, and under --…
Annactswell Sep 15, 2026
3256445
install.sh installs Docker Engine from Docker's apt repository with t…
Annactswell Sep 16, 2026
6cbe18d
The one-click upgrade runs the installer of the build it installs, fe…
Annactswell Sep 16, 2026
9e980db
upgrading.mdx says the one-click upgrade runs the installer of the ve…
Annactswell Sep 16, 2026
b0c59b4
The node's check-in says it launches the new build's install.sh when …
Annactswell Sep 16, 2026
7eb7b9c
The gateway exam's note on checkUpgrade names what it fetches now: th…
Annactswell Sep 16, 2026
104478f
A told node has fulfilled its tell when it reports another commit tha…
Annactswell Sep 16, 2026
1310400
A node's auth.ts is the fleet token and nothing else: the scrypt, ses…
Annactswell Sep 16, 2026
ec5eae3
The node's sandbox census is one GROUP BY, not every row loaded and c…
Annactswell Sep 16, 2026
6b48047
A sandbox's nodeId on the wire is the id of the node answering for it…
Annactswell Sep 16, 2026
5ab997f
A node told to upgrade owes the launch until it has made one: refused…
Annactswell Sep 16, 2026
8d309fd
upgrading.mdx says a node told while its previous upgrade is still fi…
Annactswell Sep 16, 2026
c78f059
A node's tell is one value on the gateway — when, and on what commit …
Annactswell Sep 16, 2026
04e1f4d
A node says at every check-in whether an upgrade unit is running on i…
Annactswell Sep 16, 2026
afcbda4
The updater asks systemd whether the upgrade unit is alive with list-…
Annactswell Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .changeset/fleet-install-and-upgrade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@dormice/shared": minor
"@dormice/sdk": minor
"@dormice/cli": minor
---

The fleet installs and upgrades as one. The base image is a fleet setting (`settings.baseImage`, seeded from the gateway's `DORMICE_BASE_IMAGE`, edited with `updateSettings { baseImage }`) beside a read-only `registryAddress`; both ride the configuration bundle to every node, and a node pulls an image it lacks from the fleet registry. The upgrade verbs answer at the gateway: `applyUpgrade` upgrades the gateway's machine and then every node behind it, told one at a time at its check-in; `applyUpgrade { nodeId }` tells a stuck node again; `getUpgradeStatus.nodes[]` lists each node's standing. The check-in carries `selfUpgrade` and can answer `upgrade: true`. `dor doctor` reads the fleet's base image from the environment the installer gives it and checks the fleet registry is reachable over TLS.
7 changes: 7 additions & 0 deletions .changeset/fleet-observation-at-the-gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@dormice/shared": minor
"@dormice/sdk": minor
"@dormice/cli": minor
---

The fleet-wide observation verbs answer at the gateway and say what they could not see. `listSandboxes`, `listSandboxMetrics` and `listSandboxImages` carry an optional `silent` array — the nodes the gateway could not include, with why — and the SDK's three methods now return the whole response instead of the bare array (`.sandboxes`, `.samples`, `.images`). `getHostMetrics` and `getHostMetricsHistory` take an optional `nodeId`: a machine's reading names its machine, the fleet's sums are the new `getFleetMetrics`. `getFleetTimeline` is `getFleetStateHistory` (a count per state per moment is a state sample, not a snapshot). `dor sandbox ls` warns under the table when a node did not answer.
5 changes: 5 additions & 0 deletions .changeset/gateway-check-in-and-lookup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dormice/shared": minor
---

Wire schemas for a fleet behind a gateway: `lookupSandbox` (a node answers "do you hold this sandbox?" by name or id), the node check-in (`checkInRequestSchema`, readings, build; its `endpoint` is an origin — `endpointSchema` drops a trailing slash and refuses a path), and the gateway's `listNodes` / `removeNode`. The host-metrics schema is split into named parts (`hostReadingSchema`, `dataDiskSchema`, `sandboxStateCountsSchema`) that `hostMetricsResponseSchema` still composes unchanged.
55 changes: 34 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/BitMiracle-AI/Dormice/actions/workflows/ci.yml/badge.svg)](https://github.com/BitMiracle-AI/Dormice/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)

**The SQLite of agent sandboxes** — a self-hosted sandbox platform for AI agents. One machine, sandboxes that live forever, idle costs nothing.
**The SQLite of agent sandboxes** — a self-hosted sandbox platform for AI agents. One machine or a fleet of them, sandboxes that live forever, idle costs nothing.

> **Status: early development.** The daemon, its lifecycle engine, the SDK, the CLI, the web console, the real Docker + gVisor executor, the S3 archiver, and the E2B-compatible API work end to end — the full create → freeze → stop → archive → restore cycle, command execution, file I/O, and the official `e2b` SDK against real infrastructure. Nothing here is ready for production yet.

Expand Down Expand Up @@ -31,6 +31,13 @@ a battery of read-only checks — three of them boot a real gVisor
container — that decides whether the install actually succeeded;
`dor doctor` can be re-run on its own at any time.

A second machine joins the same fleet with one more command
(`--role node --gateway http://<first machine>:80`, the token in the
environment) and needs no settings of its own; upgrades then run from
the gateway, one node at a time. See the
[installation](website/content/docs/installation.mdx) and
[upgrading](website/content/docs/upgrading.mdx) docs.

## Quick start

`@dormice/sdk` is the native TypeScript client. (Not on npm yet — the
Expand All @@ -40,7 +47,7 @@ first release is queued; inside this repo, `pnpm build` produces it.)
import { Dormice } from '@dormice/sdk';

const client = new Dormice({
endpoint: 'http://127.0.0.1:3676',
endpoint: 'http://127.0.0.1:3677',
token: process.env.DORMICE_API_TOKEN!,
});

Expand Down Expand Up @@ -93,8 +100,8 @@ import { Sandbox } from 'e2b';

const sbx = await Sandbox.create({
apiKey: `e2b_${process.env.DORMICE_API_TOKEN}`,
apiUrl: 'http://127.0.0.1:3676/e2b/api',
sandboxUrl: 'http://127.0.0.1:3676/e2b/envd',
apiUrl: 'http://127.0.0.1:3677/e2b/api',
sandboxUrl: 'http://127.0.0.1:3677/e2b/envd',
});
```

Expand Down Expand Up @@ -135,28 +142,30 @@ Deliberate deltas from the hosted product:

## Web console

The daemon serves a small web console at `http://127.0.0.1:3676/console` —
sign in with the API token once and it becomes an httpOnly session cookie;
the token itself is never stored anywhere the page can read. The console
The gateway — the fleet's front door, installed beside the daemon — serves
a small web console at `http://127.0.0.1:3677/console`: sign in with the
API token once and it becomes an httpOnly session cookie; the token itself
is never stored anywhere the page can read. The console
shows every sandbox with its live lifecycle state (the same
`/listSandboxes` the SDK sees), opens a per-sandbox detail view, creates
sandboxes (the same idempotent `acquire`, with the lifecycle knobs),
releases them, and has a Connect page with copy-paste snippets for every
client (E2B SDK, native SDK, CLI) pointed at your own endpoint.

The daemon listens on 127.0.0.1 only, so reaching it from another machine
is a choice you make explicitly, one of two ways:
Both processes listen on 127.0.0.1 only (the gateway on 3677, the daemon
on 3676), so reaching them from another machine is a choice you make
explicitly, one of two ways:

- **SSH tunnel** (private, zero setup):
`ssh -L 3676:127.0.0.1:3676 root@host`, then open
`http://127.0.0.1:3676/console`.
`ssh -L 3677:127.0.0.1:3677 root@host`, then open
`http://127.0.0.1:3677/console`.
- **Reverse proxy** for the console, the API, and the E2B surface at once —
e.g. Caddy, which also handles TLS certificates automatically once you
give it a domain:
e.g. Caddy pointed at the gateway, which also handles TLS certificates
automatically once you give it a domain:

```
your-domain.example {
reverse_proxy 127.0.0.1:3676 {
reverse_proxy 127.0.0.1:3677 {
flush_interval -1
}
}
Expand All @@ -169,8 +178,9 @@ is a choice you make explicitly, one of two ways:

## Cold archive (S3, optional)

Set the four `DORMICE_S3_*` variables and idle sandboxes take the last
step down: a week after stopping (tunable per sandbox via
Configure a store in the console's settings page — or seed the four
`DORMICE_S3_*` variables in the gateway's env (`/etc/dormice/gateway.env`)
— and idle sandboxes take the last step down: a week after stopping (tunable per sandbox via
`archiveAfterSeconds`), the disk is packed with `tar` + `zstd`, shipped to
any S3-compatible bucket (AWS, Cloudflare R2, MinIO, Alibaba OSS in
S3-compat mode), and freed locally. The next `acquireSandbox` answers
Expand Down Expand Up @@ -212,17 +222,19 @@ verifies it, but these are the facts underneath:
disable inter-container traffic (`"icc": false` in `daemon.json`). The
daemon binds to 127.0.0.1 only, by design without a knob; exposing it is
a reverse proxy's job.
- **One machine, one daemon.** The daemon enforces this with a lock next to
- **One daemon per machine.** The daemon enforces this with a lock next to
its ledger and refuses to start when its ledger and the machine's reality
cannot belong together.

## When Dormice is the wrong tool

Pick something else if:

- **You need a fleet.** One machine, one daemon, by design — that is where
the simplicity comes from. Multi-machine sharding is a future direction
(the schema already carries the fields), not a current feature.
- **You need sandboxes that move between machines, or a fleet across
regions.** A fleet is one gateway and the machines on its private
network — sharded, not distributed, by design: a sandbox lives on the
machine it was created on, and a node that is down takes its sandboxes
with it until it is back.
- **You want a managed service.** No hosted anything, no SLA. That is
E2B's product, and it is good at it.
- **Your threat model demands hardware virtualization.** Sandboxes are
Expand All @@ -242,7 +254,8 @@ pnpm monorepo:
| `packages/server` | The daemon: Fastify + SQLite ledger + lifecycle engine |
| `packages/sdk` | `@dormice/sdk` — TypeScript client for the native API |
| `packages/cli` | `dormice` command-line tool (`dor` for short) |
| `packages/console` | Web console: React SPA, served by the daemon at `/console` |
| `packages/console` | Web console: React SPA, served by the gateway at `/console` |
| `packages/gateway` | The fleet's one door in front of one or more daemons: holds the fleet's settings, templates, API keys and the console; places new sandboxes, finds existing ones by asking the nodes, forwards everything else |
| `e2e` | Black-box suite: boots the built daemon, drives it over the wire |
| `examples` | Runnable demos: the native SDK, the official `e2b` package, a resident agent |

Expand Down
30 changes: 30 additions & 0 deletions deploy/dormice-gateway.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Dormice gateway: the fleet's one door in front of one or more daemons —
# the fleet's configuration, its API keys and the web console live here,
# the sandbox verbs are forwarded to the node that holds the sandbox.
# Installed by deploy/install.sh beside the daemon (a single machine is a
# fleet of one); configuration lives in /etc/dormice/gateway.env (full-line
# comments only there — systemd's EnvironmentFile treats an inline comment
# as part of the value). install.sh restarts this unit before the daemon's,
# so both run one commit and the daemon's first check-in lands on the new
# gateway. A machine of its own joins as a node with `install.sh --role
# node --gateway http://<this machine>:80` — this machine's Caddy, the
# gateway's door; the gateway itself listens on loopback only — and runs
# no gateway unit.
[Unit]
Description=Dormice gateway (fleet front door)
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/local/bin/node /opt/dormice/packages/gateway/dist/main.js
EnvironmentFile=/etc/dormice/gateway.env
# Crash-only: the gateway holds no sandbox state — nodes check in again
# within seconds and the cache refills by asking — so restarting it is
# always safe.
Restart=always
RestartSec=3
# One process per database file. The SQLite lock enforces this; never wrap
# the gateway in a process manager that forks workers.

[Install]
WantedBy=multi-user.target
27 changes: 27 additions & 0 deletions deploy/dormice-registry.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dormice image registry: the fleet's one image store, beside the gateway
# (design record #33). The base image install.sh builds is pushed here
# once, template images are pushed by the operator, and every node pulls
# an image it lacks from here under the fleet token (the registry's
# htpasswd holds the same credential as user `dormice`). Standard CNCF
# distribution, the static binary pinned and checksummed by install.sh,
# serving TLS with a self-signed certificate whose SAN is this machine's
# address — the same file is trusted by Docker on the gateway machine
# (/etc/docker/certs.d) and copied to each node when it joins. Config in
# /etc/dormice/registry/registry.yml, images under
# /var/lib/dormice-gateway/registry. OTEL_TRACES_EXPORTER=none: the
# binary otherwise ships traces to localhost:4318 at every request and
# logs the connection refusals.
[Unit]
Description=Dormice image registry (fleet image store)
Wants=network-online.target
After=network-online.target
Before=dormice-gateway.service

[Service]
ExecStart=/usr/local/bin/registry serve /etc/dormice/registry/registry.yml
Environment=OTEL_TRACES_EXPORTER=none
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
16 changes: 11 additions & 5 deletions deploy/dormice.service
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# Dormice daemon. Installed by deploy/install.sh; configuration lives in
# /etc/dormice/env (full-line comments only there — systemd's EnvironmentFile
# treats an inline comment as part of the value).
# Dormice daemon — the node that runs the sandboxes. Installed by
# deploy/install.sh; configuration lives in /etc/dormice/env (full-line
# comments only there — systemd's EnvironmentFile treats an inline comment
# as part of the value). It takes the fleet's configuration from its
# gateway at check-in and, holding no copy yet, waits for the gateway
# before it listens — hence After= the gateway unit; not Requires=, since
# a node on a machine of its own (`install.sh --role node`) has no local
# gateway unit — After= a unit that does not exist is nothing — and keeps
# retrying the remote one on its own.
[Unit]
Description=Dormice daemon (agent sandbox control plane)
Description=Dormice daemon (agent sandbox node)
Wants=network-online.target
After=network-online.target docker.service
After=network-online.target docker.service dormice-gateway.service
Requires=docker.service

[Service]
Expand Down
Loading
Loading