recipes: map plus an optional default:. Registries
resolve bundled → ~/.dabs/recipes.yaml (global) → ./dabs.yaml
(project), later winning by name.
Field notes
- image — a bare name reuses
~/.dabs/images/<name>(built from a bundled image recipe on first use). An inline{dockerfile, context}builds withdabs build; the image is named after the recipe, so two recipes sharing one Dockerfile still build two tags. - command — must not bake in agent instructions; recipes provision, callers prompt.
- sources — exactly one origin kind per entry (
mount/mkmount/copy/worktree). Space variables$NODE_VOLUME|HELD|TMPand$PARENT_VOLUME|HELD|TMPname HOST origins, and substitute in a source’s origin path only. ($NODE_EPHEMERAL/$PARENT_EPHEMERALare permanent aliases for the held variants.) Mounts land parent-before-child regardless of declaration order. $NODE_IDin box paths — a source’s destinationpath:and the recipe’sworkdir:may name$NODE_ID(or${NODE_ID}), which expands to the id of the box being provisioned, so a recipe can auto-namespace what it mounts per box (path: /$NODE_ID). It is the only variable a box path may name — a space variable mixed into one is still rejected — and the id is validated as a slug before it is substituted, so$NODE_IDcan never become a traversal out of the directory the path names. See Sources.- publish — grants the box the ability to publish services (
forward publish <name> --type webui|general --port <n>inside it, reached from the host withdabs services). It grants the ABILITY, not a list of names: what a box publishes is chosen at run time by the program in it. dabs realizes the grant as the box’s door — one dabs-owned unix socket, answered on the host by a relay that lives as long as the box’s node — so it needs a box (a recipe with noimage:is refused) and a local target (a box on another machine has no path to a relay on this host). Without the grant there is no door, andforward publishin the box refuses by name instead of failing on a missing file. - user — who the box’s processes run as, written
uidoruid:gid, and numeric: a name is resolved in the image’s own/etc/passwd, so it would say nothing about which host user ends up owning what the box writes. Unset leaves the image’s user, which for an image naming noUSERis root. Set it when the HOST reads what the box writes: amount:/mkmount:carries uids, not names, so under the docker driver — whose daemon is root — a root box leaves root-owned files that the ordinary user who booted it cannot open. The bwrap and apple drivers need nothing here and ignore it: a bwrap box is already a process of your own inside a namespace that maps only your uid, and the apple driver’s micro-VM translates ownership across its own boundary (and its boxes must be root to open a relayed socket at all). - target —
""(local) or a server fromdabs servers. The reservedINTERNAL-docker-privileged-for-nested-sandboxingtarget exists for boxes that must boot nested sandboxes. - boxless recipes — a recipe with no
image:provisions its places (a worktree, a copied dir) and stops;wtandscratchare the bundled examples.
Egress
egress controls the box’s outbound network. As a scalar it is open (full
outbound, the default when unset) or none (no network). As a mapping it
routes ALL egress through the dabs proxy engine, which enforces two independent
layers:
- Policy —
allow/deny(protocol-agnostic). The engine checks the CONNECT host against these patterns on the plaintexthost:port, before any tunnel, for every protocol (HTTPS, WebSocket, raw TCP — HTTP cannot skip it).allowdefault-denies the rest;denydefault-allows it; they are mutually exclusive. A pattern isexample.com(exact),*.example.com(any subdomain, not the apex), or*(all). Write a comma-separated string or a YAML list. - Content —
http_proxy(HTTP/1.1 only). An ordered chain, box→internet, oftlsboundary directives and hookmodules. Atls: terminateopens a plaintext window (optionally scoped todomains:) that atls: originatecloses; module hooks inside it inspect decrypted HTTP through four streaming verbs (onRequest,onRequestChunk,onResponse,onResponseChunk). Bodies stream through chunk by chunk — the engine never buffers. Non-HTTP traffic inside a terminate window (h2, WebSocket over TLS, any raw protocol) is re-originated untouched, so terminating a domain never costs the box its other protocols to it. Module paths resolve like sources: relative to the recipe’s owndabs.yaml. - Connection — a
moduleOUTSIDE anytlswindow (protocol-agnostic). Such a hop sees no content; it acts on the connection through a fifth verb,onConnect({host, port}, ctx), returning nothing (or{action: "allow"}) to let the dial through and{action: "deny", reason}to refuse it with a 403. It runs on both proxy paths after theallow/denygate has already allowed the host, so it can only narrow the recipe’s policy, never widen it. Hops run chain order and the first deny wins; a hook that throws, hangs, or answers with anything else denies the connection. A refusal is recorded in the connection ledger — the hop’s name and its length-capped reason — and nowhere else. A hop out here that also exports content verbs is warned about at boot: those verbs still need atls: terminateto run.
bun and openssl on the host PATH, and a forwarder binary
to mount in the box: the CLI carries one embedded (-tags withforwarder), and a
program embedding dabs as a Go module — which has no embed — supplies its own
with actions.Real.WithForwarder(path), any binary speaking the forwarder
protocol. An explicit supply wins over an embed. Without either, open/none
still work and proxy egress fails clearly at boot.
egress: open has a host dependency of its own on Linux (the bwrap driver): the
box’s network namespace is built by pasta, so the passt package must be
installed and dabs must run as an unprivileged user. Without either, an
open-egress boot fails clearly, naming the package. pasta must be snapshot 2025_05_03 or newer, for the address flags dabs passes: Debian trixie+ and Fedora 41+ package a new enough one, Ubuntu’s and Alpine’s current packages refuse those flags, and there you build from passt.top at the version contrib/recipes/dabseption.Dockerfile pins. egress: none needs
nothing.
The linux drivers mount that binary into the box. The apple driver mounts no
host binary — a host one cannot run in the linux micro-VM — so there the image
carries the forwarder at /run/dabs/forward (the boot probes for it, and the
error names the Dockerfile lines that add it), and the image’s copy is what
runs.