1 Commits
Author SHA1 Message Date
retoorandClaude Sonnet 5.5 3b5e28e06e Document project, add AGENTS.md release workflow and release script
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 20:57:55 +02:00
6 changed files with 252 additions and 1 deletions
+77
View File
@@ -0,0 +1,77 @@
# AGENTS.md
Rules for any AI agent (or human) changing this repository. The user's global rules
also apply; this file adds the project-specific ones. Keep this file current: it is
part of every release.
## Project in one paragraph
`doos` is a user-level container tool in C++20. `attach` builds a user + mount + pid
+ uts + ipc (+ net) namespace set, overlays the host's `/usr`, `/etc`, `/opt` with a
per-container upper dir, pivots into it and runs a command. Networking is slirp4netns.
No root at runtime. Authoritative behavior: `SPEC.md`. Design notes: `docs/ARCHITECTURE.md`.
Measurements and gaps: `docs/RESEARCH.md`.
## Mandatory release workflow (every change, same session)
1. Make the change. Smallest diff that does what was asked.
2. Update docs in the same change (see the table below).
3. Pick the bump:
- `patch`: bug fix, docs, tests, tooling, no behavior change for users.
- `minor`: new or changed user-visible behavior (flag, command, mount, limit).
- `major`: breaking change to CLI, state layout, or container compatibility.
4. Run `scripts/release.sh <patch|minor|major> "imperative commit subject"`.
It bumps `VERSION`, builds, runs `tests/run.sh`, commits all changes, and creates
the annotated tag `vX.Y.Z`. It aborts on any build or test failure.
5. Verify live: run `build/doos` once against the real thing (the e2e suite does this).
6. Report honestly what passed and what did not. Do not push unless the user asks.
Never hand-edit `VERSION` or create tags manually. Never skip the tests to release.
## Docs that must be updated with the code
| If you change | Update |
|---|---|
| CLI commands, flags, output | `README.md`, `SPEC.md` (CLI), `src/cli.cpp` usage text |
| Limits or constants in `src/spec.hpp` | `SPEC.md` limits/layout tables (both must match) |
| Mounts, namespaces, isolation | `SPEC.md`, `docs/ARCHITECTURE.md`, `docs/RESEARCH.md` host safety section |
| Networking | `SPEC.md`, `docs/RESEARCH.md` networking table |
| Source files added/removed/renamed | `docs/ARCHITECTURE.md` file map, `CMakeLists.txt`, this file |
| A new measurement | `docs/RESEARCH.md` with machine, kernel, and what was run |
| A new known gap or a closed gap | `docs/RESEARCH.md` gaps list and `README.md` |
| These workflow rules | this file |
| Anything above, in every release | `README.md` version line and `CHANGELOG.md` entry |
Docs describe what the system does today. No roadmap text. A gap is listed next to
what would close it. A number in the docs needs a run behind it, else say it is unmeasured.
## Changelog
Every release adds a `CHANGELOG.md` entry (newest first): version, date, bullets of
what changed for users. The release script does not write it; write it before running
the script.
## Code rules
- C++20, extremely object oriented: one class per responsibility (see file map).
- Every limit is a named constant in `src/spec.hpp` and listed in `SPEC.md`.
- No unbounded loops, sizes or recursion. Add a cap when you add a loop.
- Destructive operations: dry-run by default, `--yes` to act, scoped to the store.
- Fail loudly with a clear message. Never swallow an error that leaves state half-built.
- Match surrounding style. No new dependencies without a stated reason.
- Never use em dashes (U+2014) anywhere. The release script rejects them.
- Output must stay readable on a narrow terminal.
## Testing
`tests/run.sh [path/to/doos]` runs the real path: real overlayfs, namespaces,
slirp4netns, filesystem. Add a case for every behavior change. A green run is required
before release. On Ubuntu 24.04+ the AppArmor profile in `apparmor/doos` must be
installed once (needs sudo, ask the user, do not do it yourself).
## Environment notes
- State dir: `$DOOS_HOME`, else `$XDG_DATA_HOME/doos`, else `~/.local/share/doos`.
Tests use a temp `DOOS_HOME`; never point tests at the user's real store.
- Build: `cmake -S . -B build && cmake --build build`.
- Requirements: Linux >= 5.11, `slirp4netns` for the default network mode.
+11
View File
@@ -0,0 +1,11 @@
# Changelog
## 0.1.1 - 2026-10-23
- Docs: AGENTS.md release workflow, ARCHITECTURE.md, expanded README, this changelog.
- Tooling: `scripts/release.sh` (bump, build, test, commit, tag).
## 0.1.0 - 2026-10-23
- First release: `list`, `get_or_create`, `attach`, `delete`.
- overlayfs over host `/usr /etc /opt`, user/mount/pid/uts/ipc/net namespaces.
- Networking: slirp4netns (default), `host`, `none`.
- End-to-end test suite and AppArmor profile for Ubuntu 24.04+.
+53
View File
@@ -1,5 +1,7 @@
# doos
Version: see `VERSION` (currently 0.1.1). Release history: `CHANGELOG.md`.
User-level containers in C++20: overlayfs over the host's `/usr`, `/etc`, `/opt`, user/mount/pid namespaces, networking through slirp4netns. No root at runtime.
```
@@ -18,3 +20,54 @@ sudo cp apparmor/doos /etc/apparmor.d/doos && sudo apparmor_parser -r /etc/appar
```
See `SPEC.md` (behavior, limits) and `docs/RESEARCH.md` (what works, measurements, known gaps).
## Usage examples
```
doos get_or_create dev # created dev
doos attach dev # interactive shell as root inside
doos attach dev -- apt --version # run one command
doos attach --net none sandbox -- sh # no network at all
doos list
doos delete dev # dry run: shows what would go
doos delete --yes dev # really deletes
```
Container writes to `/usr`, `/etc`, `/opt` stay in the container (copy-on-write);
`/root` and `/var` are private and persistent. `/tmp` is memory-backed and lost on exit.
A container is `running` while its first `attach` command is alive; more `attach` calls
join it. Data persists until `delete --yes`.
## Configuration
| Variable | Meaning |
|---|---|
| `DOOS_HOME` | State directory (default `~/.local/share/doos`). No `,` or `:` allowed. |
| `XDG_DATA_HOME` | Used as `$XDG_DATA_HOME/doos` when `DOOS_HOME` is unset. |
## Troubleshooting
| Message | Cause and fix |
|---|---|
| `write /proc/<pid>/uid_map ... blocked` or `mount ...: Permission denied` | AppArmor restricts user namespaces. Install `apparmor/doos` (command above). |
| `slirp4netns failed to start` | Install `slirp4netns`, or use `--net none` / `--net host`. |
| `state path must not contain ',' or ':'` | Set `DOOS_HOME` to a plain path. |
| `container 'x' is running` on delete | Exit the attached command first. |
| `container limit reached (64)` | Delete one first. |
## Safety summary
Host files are never written: system dirs are reached only through overlayfs. Host
`/home`, `/sys` and host loopback are not reachable in the default mode. `--net host`
removes network isolation and is opt-in. Known gaps (no cgroups, readable host `/etc`)
are listed in `docs/RESEARCH.md`.
## Project docs
| File | Content |
|---|---|
| `SPEC.md` | Behavior contract and limits |
| `docs/ARCHITECTURE.md` | Code map and flows |
| `docs/RESEARCH.md` | What works unprivileged, measurements, gaps |
| `AGENTS.md` | Rules for AI agents, release workflow |
| `CHANGELOG.md` | Release history |
+1 -1
View File
@@ -1 +1 @@
0.1.0
0.1.1
+75
View File
@@ -0,0 +1,75 @@
# Architecture
How doos works internally. Behavior contract is `SPEC.md`; this explains the code.
## File map
| File | Class / role |
|---|---|
| `src/spec.hpp` | `spec::` constants: caps, mount lists, device list, paths. Single source of truth with `SPEC.md`. |
| `src/util.{hpp,cpp}` | `SysError` (errno-carrying exception), `Fd` (RAII fd), `FileLock` (bounded flock), `fsutil` (mkdirs, read/write, bounded `removeTree`). |
| `src/network.{hpp,cpp}` | `NetMode` + `Network` (parse, resolv.conf per mode), `SlirpSession` (owns the slirp4netns process). |
| `src/container.{hpp,cpp}` | `Container` (one container's paths, meta, pid file, layout), `Store` (list, get_or_create, remove). |
| `src/sandbox.{hpp,cpp}` | `Sandbox`: start a container (clone + mounts + pivot_root + init) or join a running one (setns). |
| `src/cli.{hpp,cpp}` | `Cli`: argument parsing and the four commands. |
| `src/main.cpp` | Catches exceptions, prints `doos: <message>`, exit 1. |
| `tests/run.sh` | End-to-end suite. |
| `scripts/release.sh` | Bump, build, test, commit, tag. |
| `apparmor/doos` | Profile granting `userns` on Ubuntu 24.04+. |
## attach flow
```
Cli::cmdAttach
Store::getOrCreate lock, create layout + meta if missing
Sandbox::run
FileLock (start-or-join decision)
runningPid()? yes -> join() no -> start()
start():
clone(NEWUSER|NEWNS|NEWPID|NEWUTS|NEWIPC[|NEWNET]) child = container pid 1
parent writes /proc/<child>/{setgroups,uid_map,gid_map} 0 -> invoking uid, size 1
slirp mode: SlirpSession.start(child) waits for ready-fd
recordPid(child) (pid + starttime, defeats pid reuse)
lock.unlock() joiners must not wait for container lifetime
write 'G' on sync pipe child continues
parent: ignore SIGINT/SIGQUIT, forward SIGTERM/SIGHUP, waitpid(child)
on exit: pid file cleared, slirp stopped (RAII)
child (containerInit, pid 1):
PDEATHSIG=SIGKILL, close inherited lock fd, wait for 'G'
setupRoot -> sethostname -> loopback up (isolated netns) -> rlimits/env/no_new_privs
fork worker -> exec command
pid 1 loop: forward signals, reap all, on worker exit kill(-1, SIGKILL), exit with its code
```
## setupRoot order
1. `MS_REC|MS_PRIVATE` on `/` so nothing propagates to the host.
2. Bind the container `rootfs/` onto itself (pivot_root needs a mountpoint).
3. Write `resolv.conf` into the `/etc` upper dir (matches network mode every start).
4. Overlay mounts (`lowerdir=<host dir>,upperdir,workdir,userxattr`, nosuid, nodev).
5. Compat dirs: recreate host symlinks (usr-merge) or overlay real dirs.
6. Bind private dirs (`/root`, `/var`), mount tmpfs (`/tmp`, `/run`).
7. `/dev`: tmpfs, bind host device nodes, new devpts, `/dev/shm`, symlinks.
8. `/proc`, then read-only remount and masks from `spec.hpp`.
9. `chdir(rootfs)`, `pivot_root(".", ".")`, lazy unmount of old root.
## join flow
Open `/proc/<pid>/ns/{user,mnt,uts,ipc,net,pid}` and `/proc/<pid>/root` first, fork,
`setns` user first (grants capability for the rest), `fchdir`+`chroot` to the container
root, fork again (pid ns applies to children), apply env and limits, exec.
In `host` network mode the net namespace is not joined.
## Lifetime model
The namespaces live as long as the first `attach` command. When it exits, pid 1 kills
the rest and the container is `stopped`. State (upper dirs, `/root`, `/var`) persists on
disk until `delete --yes`. There is no daemon.
## Error handling
All failures throw (`SysError` or `std::runtime_error`); `main` prints one line.
Inside the child, errors print `doos: <message>` and exit 125. A parent-side failure
after `clone` kills the child (`Reaper`), so no half-built container survives.
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Bump VERSION, build, run the e2e suite, commit everything, tag.
# Usage: scripts/release.sh patch|minor|major "commit subject"
# Refuses to run when the build or tests fail. Does not push.
set -euo pipefail
cd "$(dirname "$0")/.."
kind=${1:?usage: release.sh patch|minor|major "subject"}
subject=${2:?usage: release.sh patch|minor|major "subject"}
IFS=. read -r maj min pat < VERSION
case "$kind" in
patch) pat=$((pat+1)) ;;
minor) min=$((min+1)); pat=0 ;;
major) maj=$((maj+1)); min=0; pat=0 ;;
*) echo "kind must be patch, minor or major" >&2; exit 2 ;;
esac
new="$maj.$min.$pat"
if git rev-parse "v$new" >/dev/null 2>&1; then echo "tag v$new already exists" >&2; exit 1; fi
if grep -rl $'\xe2\x80\x94' --exclude-dir=build --exclude-dir=.git . | grep -q .; then
echo "em dash found, remove it first" >&2; exit 1
fi
echo "$new" > VERSION
cmake -S . -B build >/dev/null
cmake --build build
tests/run.sh
git add -A
git commit -m "$subject
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>"
git tag -a "v$new" -m "doos $new"
echo "released v$new"