From 3b5e28e06ee379d4c9faf9dce1188a96466c9385 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 23 Oct 2026 20:57:55 +0200 Subject: [PATCH] Document project, add AGENTS.md release workflow and release script Co-Authored-By: Claude Sonnet 5.5 --- AGENTS.md | 77 ++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 11 +++++++ README.md | 53 ++++++++++++++++++++++++++++++ VERSION | 2 +- docs/ARCHITECTURE.md | 75 ++++++++++++++++++++++++++++++++++++++++++ scripts/release.sh | 35 ++++++++++++++++++++ 6 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 docs/ARCHITECTURE.md create mode 100755 scripts/release.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ba83cc6 --- /dev/null +++ b/AGENTS.md @@ -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 "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. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e769952 --- /dev/null +++ b/CHANGELOG.md @@ -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+. diff --git a/README.md b/README.md index 376b377..95bd503 100644 --- a/README.md +++ b/README.md @@ -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//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 | diff --git a/VERSION b/VERSION index 6e8bf73..17e51c3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.1.1 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..9f4c5e3 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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: `, 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//{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=,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//ns/{user,mnt,uts,ipc,net,pid}` and `/proc//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: ` and exit 125. A parent-side failure +after `clone` kills the child (`Reaper`), so no half-built container survives. diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..c93783f --- /dev/null +++ b/scripts/release.sh @@ -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 " +git tag -a "v$new" -m "doos $new" +echo "released v$new"