The wrapper's config writing is gone: --env-file FILE selects the target instance (default .env; named like docker's flag — --env means a single variable there), the init fragment named by WERKATOR_INIT_CONFIG is uploaded and installed remotely via 'werkator init --apply', and instance-start places the .htaccess that init now generates. The heredoc/sed machine-config writing is deleted; control-token delegates to the werkator CLI; check-prerequisites uploads werkdock and runs its doctor — tools/werkator-build-prerequisites.sh retires. The idle-check and port-forward port lookups read the effective config via config:print, so a port living in the applied fragment is found too. Verified live against mih34 with the .env.mih34 + .env.mih34.yml pair (gitignored via the new /.env.* rule): update, doctor PASS 6/6, repo-init applying the fragment as the new layer, instance-start placing the generated proxy and restarting the unit, control-token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
4.2 KiB
Markdown
62 lines
4.2 KiB
Markdown
# Bubblewrap User-Namespace Sandbox as the Third Build Runtime
|
|
|
|
**Status:**
|
|
- proposed: 2026-08-10
|
|
- accepted: 2026-09-01
|
|
- rejected: -
|
|
- superseded: -
|
|
|
|
**Decision [accepted]:** On hosts without root and without a Docker daemon — Hostsharing Managed Webspaces — builds run inside a `bwrap` (bubblewrap) user-namespace sandbox over a prepared rootfs archive, implemented by `BwrapBuildRunner` as the third runtime behind the `BuildRunner` interface.
|
|
Filesystem isolation only: network, uid mapping, `/proc`, `/dev`, and `/tmp` are shared with the host by contract.
|
|
|
|
Note: plan step 17 announced this decision as "ADR 0007", but 0007 was taken by the build-definitions decision on 2026-08-28; it is recorded here as 0008.
|
|
|
|
## Context and Problem Statement
|
|
|
|
Werkator's build sandbox was Docker (ADR 0004ff., plan step 11) or nothing (`native`).
|
|
Managed Webspaces provide neither root nor a Docker daemon, so a Werkator instance there could only build with the host's own toolchains — no isolation, and no way to install the toolchain versions a project needs.
|
|
The platform does provide unprivileged user namespaces and ships `bubblewrap 0.8.0` (verified on h68, kernel 6.1), which allows mounting a self-prepared root filesystem without any privilege.
|
|
|
|
### Technical Background
|
|
|
|
`BwrapBuildRunner` shells out to the `bwrap` CLI — same pattern as git and docker, no library.
|
|
The rootfs comes from a project-built archive (`tools/build-bwrap-rootfs.sh`), unpacked on demand into `.git/werkator/buildenv/<envKey>/rootfs` and bound read-only at `/`; uid 0 inside maps to the calling user.
|
|
The git metadata mounts of step 16 are reused unchanged: read-only `.git`, tmpfs mask over `.git/werkator/`, read-write worktree admin directory — so secrets stay outside the sandbox exactly as in Docker builds.
|
|
`bwrap` creates bind mountpoints against the sandbox view, so every mountpoint must exist in (or be pre-created in) the rootfs; the runner handles that.
|
|
Config: `bwrap.enabled`/`bwrap.rootfs` are pinned like the docker sandbox policy — a branch can never turn its sandbox off or swap the rootfs; docker and bwrap are mutually exclusive per build and rejected loudly, never picked silently.
|
|
Floor: bubblewrap 0.8.0 has no `--overlay` (added in 0.9.0), so throwaway writable layers are built from tmpfs/bind mounts, not overlays.
|
|
|
|
## Considered Options
|
|
|
|
* bwrap user-namespace sandbox (prepared rootfs, filesystem isolation only)
|
|
* proot / fakechroot (syscall- or libc-level path rewriting)
|
|
* plain native with hand-installed toolchains (no isolation)
|
|
|
|
### bwrap User-Namespace Sandbox
|
|
|
|
Good:
|
|
|
|
- Real kernel-level mount isolation without root; works with what the platform already ships.
|
|
- The prepared-rootfs model gives every project its own toolchain versions, like a Docker image does.
|
|
- Shelling out to a CLI matches the existing git/docker access pattern; the attached process supports log streaming and cancellation unchanged.
|
|
|
|
Bad:
|
|
|
|
- The rootfs archive is project infrastructure that must be built and uploaded (~1.5 GB unpacked, disk/quota checked by `werkdock doctor`, which ported the original prerequisites script).
|
|
- Filesystem-only isolation: network and process view are the host's — acceptable here, and pinned so a branch cannot widen it, but weaker than Docker.
|
|
- Mountpoint pre-creation and mount ordering are subtle (hardened on the real webspace; see the fix messages preserved in commit `71f1fc6`).
|
|
|
|
### proot / fakechroot
|
|
|
|
Rejected: syscall tracing (proot) is an order of magnitude slower and historically fragile with modern toolchains; fakechroot's `LD_PRELOAD` path rewriting breaks on statically linked tools and does not isolate anything the kernel enforces.
|
|
|
|
### Plain Native with Hand-Installed Toolchains
|
|
|
|
Rejected: no isolation, host pollution, and toolchain versions become webspace-global instead of per project — exactly the situation the sandbox exists to end.
|
|
|
|
## Decision Outcome
|
|
|
|
bwrap, as implemented in PR #4.
|
|
The generic sandbox machinery (rootfs build, prerequisites check, invocation logic) is planned to be extracted into the standalone tool **Werkdock** (plan step 21); `BwrapBuildRunner` will then delegate to the `werkdock` CLI.
|
|
That extraction changes the executor behind the config keys, not this decision.
|