Skip to content

Latest commit

 

History

161 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dabs

test release license go docs

dabs — dumb agent boxes

Disposable spaces you can hand to an AI agent: a sandbox, a git worktree, a throwaway copy of your project. Declare a space once as a recipe, then summon it by name — dabs recipe <name> — locally or on a remote server. dabs keeps a record of every space it provisions, so nothing an agent produced is ever lost, or left lying around, silently.

Requirements

dabs drives platform tools you install yourself. It detects them and points you at the install command; it never installs anything for you.

  • macOS on Apple Silicon — Apple's container CLI. Each box is a Linux micro-VM. brew install container && container system start
  • Linuxbubblewrap (enters boxes) + docker (builds images). Boxes are bwrap + overlayfs. apt install bubblewrap · docker: https://docs.docker.com/engine/install/
  • Remote servers (any of the above, driven over ssh) — ssh with pubkey auth on your side; dabs installed on the server.
  • No Windows driver.

Install

Install script — picks the right prebuilt binary, checks it against the release's SHA256SUMS, and puts it in ~/.local/bin. No root, nothing system-wide; set DABS_INSTALL_DIR to install it elsewhere.

curl -fsSL https://dabs.dev/install.sh | sh

That URL redirects to install.sh as attached to the latest release, so you can read it before you run it:

curl -fsSL https://dabs.dev/install.sh -o dabs-install.sh
less dabs-install.sh && sh dabs-install.sh

Or prebuilt binary — download from Releases, then put it on your PATH:

mkdir -p ~/.local/bin
chmod +x dabs && mv dabs ~/.local/bin/   # or anywhere on PATH

Or build from source (needs Go 1.23+):

go install github.com/jjmerino/dabs@latest

Quick start

Describe the box with two files in your project:

# dabs.yaml — the recipes: a box is a name, an image, sources, a command
recipes:
  myproj:
    image: { dockerfile: Dockerfile, context: . }
    command: [sh]
    env: { MY_FLAG: "1" }
    sources:
      - mount: .           # your cwd, live — edits persist on the host
        path: /work
# Dockerfile — what "fresh machine" means for your program
FROM ubuntu:24.04
WORKDIR /work
RUN apt-get update && apt-get install -y git

Then:

dabs build myproj                # build the box's image
dabs recipe myproj --no-command  # recipe booted: myproj (id: myproj-a3f9c21d4e02) — a NEW pristine box, no command
dabs ls                          # the tree: what dabs owns, and where it runs
dabs info myproj-a3f             # ONE node in full: location, spaces, recipe, appended argv
dabs exec myproj-a3f 'ls | wc -l'        # run a shell line inside (instance prefixes ok, git-style)
dabs exec myproj-a3f -- ./mycli --help   # exec an exact command inside (no shell)
dabs recipe myproj               # boot a box and run its command
dabs recipe myproj --detach      # boot a box, start its command in the background, return
dabs recipe myproj --name api    # same, but the node is named: exec/rm/cd by "api"
cd "$(dabs cd api)"              # jump to a node's working place (repo, checkout, or node dir)
dabs rm myproj-a3f -y            # stop it and remove its node

Every boot creates a new instance with a random id — the image is the clean state, so "give me a fresh machine" is instant. rm <name> --dry shows what a name would reap; a name matching more than one node is refused unless you pass --multiple.

Nodes

dabs marks every place it makes, so it can tell you what ran, from where, and whether anything is still live. dabs ls is that tree.

  NODE                   KIND         VOL  HELD  TMP  STATE  INFO
  myproj                 project                             ~/code/myproj
  └─ myproj-18f9c901     workdir                             ~/.dabs/nodes/myproj-18f9c901
     └─ myproj-a88626a1  box (apple)                  live   dabs exec myproj-a3f9c21d4e02 bash

Everything on this machine draws in one flat tree — a box's driver rides in its KIND cell; only a registered server gets a heading. INFO is per kind: a project's source repo, a worktree's checkout, and for a box the command that shells into it. dabs info <node> expands ONE node instead — its location, instance, the argv appended at boot, each space, and the recipe snapshot it was provisioned from.

A node is a project (the directory a command ran from), a workdir (a directory a recipe took as its .), a worktree (a git branch dabs cut), or a box. They stack: project → (workdir | worktree)? → box.

A recipe with no image makes a place and stops — dabs recipe wt cuts a worktree, no box. Point a box at one later, or never.

Each node offers three directories, and the one a recipe mounts says what happens to the bytes:

space on rm
volume/ kept, unless you ask for it by name (rm -y --volume)
held/ reaped with consent; without it, kept and its path printed
tmp/ reaped, always

A source names them with $NODE_* (this box's) and $PARENT_* (its place's — what you want back next time, since a box never returns):

- mkmount: ~/.dabs/shared/claude          # shared by every box that mounts it
  path: /root/.claude
- mkmount: $PARENT_VOLUME/claude/projects # this place's sessions; survive `rm`
  path: /root/.claude/projects

dabs rm <node> stops a box and removes its node and whatever stands on it (it brings boxes down first, and asks before it loses a live box or held data). dabs rm --keep <box> stops the box but KEEPS its node record instead. dabs ls shows only active subtrees; dabs ls --inactive shows only the inactive ones (the empty markers ls hides), and dabs rm --inactive sweeps them all in one shot (--dry previews).

Remote servers

A sandbox can live on another machine that has dabs installed (a Mac mini or Linux box sitting around), reached over ssh with pubkey auth. Register it, then point a manifest at it:

dabs servers add homelab            # host defaults to the name; or: add homelab user@10.0.0.5
dabs servers ls                     # NAME  VIA  DESTINATION
#   local     apple this machine
#   homelab   ssh homelab
dabs servers rm homelab             # unregister (remote sandboxes untouched)

Route a recipe to a server with target: homelab (omit for local). dabs build/recipe then run there; dabs ls aggregates every driver, drawing each server's boxes under a heading that names it; exec/rm address any instance by name wherever it lives.

Recipe fields (dabs.yaml)

A recipe is the whole box spec. Recipes resolve bundled → ~/.dabs/recipes.yaml (global) → ./dabs.yaml (project), later winning; a top-level default: names the recipe build/recipe use when given no name.

Five generic recipes ship bundled and work in any directory (dabs recipes lists them): sh (a shell in a clean box over the cwd), wt (cut a git worktree, no box), wtbox (a shell box over a fresh worktree), scratch (copy the cwd into a directory node, no box), scratchbox (a shell box over a throwaway copy of the cwd).

field default meaning
image (required) a bare image NAME to reuse, or {dockerfile, context} to build
description the one-line summary dabs recipes shows
workdir /work cwd inside the box (may name $NODE_ID)
command what runs in the box
env environment inside the box
sources what lands in the box, and how
target local which driver runs it
egress open outbound network: open, none, or a proxy policy (allow/deny/http_proxy)
keep false keep the box alive after the command finishes

Paths given to build/recipe may be a dabs.yaml or a directory containing one.

Sources

Each source names its origin with exactly one of four kinds, and a path inside the box:

kind what lands in the box the host
mount a live bind; the box's writes hit the host must exist (ro: true for read-only)
mkmount a live bind created (0700) if absent
worktree a fresh git branch off HEAD, mounted live your tree untouched; reap with dabs worktrees
copy a snapshot taken at box start untouched

Host paths may use ~ and $VAR. dabs also supplies the box's three node spaces to source paths — $NODE_VOLUME (survives rm unless --volume), $NODE_HELD (rm asks first; $NODE_EPHEMERAL is a permanent alias), $NODE_TMP (rm reaps quietly) — plus the $PARENT_* family naming the same spaces of the box's parent place. Use $PARENT_VOLUME for what a box wants back on the NEXT box: a fresh box gets an empty $NODE_VOLUME, but its parent place persists, so a box can keep a private slice that reloads next time:

sources:
  - mkmount: ~/.dabs/shared/claude           # a login dir every box shares
    path: /root/.claude
  - mkmount: $PARENT_VOLUME/claude/projects  # this place's sessions; reload next box, kept across `rm`
    path: /root/.claude/projects

An agent harness logs in by running the box: the first mkmount creates the dir empty, you log in once inside, and every later box that mounts it is logged in.

Design

  • cli parses argv into typed params; core/actions owns all policy; core/sandbox is the mechanical driver contract (exact names in, state out); drivers live under core/sandbox/<vendor> and are build-tagged when OS-coupled.
  • The image is the frozen fresh machine — rebuild it to change what a box carries. What crosses the boundary at runtime is exactly what the recipe's sources declare, and nothing else.

Privacy

The dabs CLI runs locally and has no telemetry or cloud features. Fetching the install script from the website is counted anonymously.

License

Apache 2.0

About

Dumb Agent Boxes, locally.

Resources

Security policy

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages