- PR-docs get their real numbers: bwrap-build-runtime -> PR#4, build-current-head-from-the-branches-view -> PR#3 (files and scenario IDs) - tools/remote: header note marking install's clone step and build as the self-build prototype, superseded by session D of step 21 (usage range grown) - ADR 0008: bubblewrap user-namespace sandbox as the third build runtime; step 17 and the plan README now point at 0008 (0007 was taken by build definitions before the step landed) - AGENTS.md: decisions list catches up with ADR 0007 and ADR 0008 - architecture skill: BwrapBuildRunner paragraph (rootfs unpack, uid mapping, mount ordering, pinned keys) and the dispatcher's three-way routing - plan README: step 21 entry now describes the werkdock/ subdirectory path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.3 KiB
WARNING: This document describes only the change applied in this PR. It may already be outdated once the next PR is merged. Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
The Problem
Werkator runs its builds on the host, either natively or inside a Docker container. A Hostsharing Managed Webspace has neither root nor a Docker daemon, so neither runtime works there — yet that is exactly where some users want to run a Werkator that builds Werkator itself.
The only sandboxing primitive available there is bwrap (bubblewrap): unprivileged user namespaces with a uid-0 mapping and read-only root binds.
Step 17 (docs/plan/17-bwrap-build-runtime.md) defines a third build runtime behind the BuildRunner interface that is based on it.
Non-Goals
- No change to the native and Docker runtimes; bwrap is added alongside them and selected per branch via config.
- No Docker inside the sandbox; Testcontainers-based tests cannot run there and are excluded by a branch's own build command.
- No overlayfs: the webspace's bubblewrap 0.8.0 predates
--overlay, so a throwaway writable rootfs per build is out of scope. - No web-access deployment; that half of step 17 (Apache
.htaccessproxy, systemd user unit, Let's Encrypt) needs a real webspace and is written up separately. - No per-repo build automation on the webspace; this PR makes it possible, and a follow-up runs it on a real host.
The Scenarios
Feature: bubblewrap as the third build runtime
Background
- A branch selects exactly one runtime: nothing (native),
docker.enabled, orbwrap.enabled. bwrap.enabledandbwrap.rootfsare pinned (host-set), so a branch's committed config cannot switch its own sandbox off or substitute a foreign rootfs.docker.enabledandbwrap.enabledare mutually exclusive per branch; enabling both is rejected, not silently picked.
Scenario#4.01: A bwrap build runs the command inside the sandbox as root
So that the build is isolated from the host exactly as the native and Docker runtimes intend.
- Given a branch with
bwrap.enabledand abwrap.rootfsarchive - When a build for that branch starts
- Then Werkator unpacks the rootfs on demand into
.git/werkator/buildenv/<envKey>/rootfs- and invokes
bwrapwith a uid-0 mapping, a read-only root bind of the rootfs, the workspace bound at its host path, and--chdirinto it - and the returned process is the attached
bwrapprocess, so streaming and cancellation behave like native builds
- and invokes
- and the environment plus
bwrap.envare passed via--setenv
Verified by
Scenario#4.02: Git metadata mounts keep secrets out of the sandbox
So that builds can run read-only git commands but never reach the machine config or the control token.
- Given a workspace that is a worktree of the repository
- When the sandbox is assembled
- Then the primary
.gitis bound read-only- and
.git/werkator/is masked by an empty tmpfs - and the worktree admin directory is bound read-write
- and
Verified by
Scenario#4.03: A branch cannot turn its sandbox off or swap its rootfs
So that the pinned sandbox policy holds for builds a branch invents as well as for ones the host already knows.
- Given a branch whose committed config sets
bwrap.enabledorbwrap.rootfs - When that config is resolved into a build
- Then the pinned keys are stripped from the worktree layer
- and enabling both
dockerandbwrapon a build is rejected, not picked silently
- and enabling both
Verified by
Scenario#4.04: The dispatcher routes builds to the bwrap runtime
So that a bwrap-explicit branch builds inside the sandbox rather than natively.
- Given a branch with
bwrap.enabled - When its build is dispatched
- Then
BwrapBuildRunneris selected
Verified by
Feature: Werkator builds itself without Docker
Background
- Werkator's own build runs the full test suite, which includes
TestcontainersSmokeTest. - On a Docker-less host (the webspace, and the bwrap sandbox that builds there), that test must not fail the self-build.
Scenario#4.05: The Testcontainers smoke test is skipped, not failed, without Docker
So that a Docker-less build of Werkator itself stays green.
- Given no reachable Docker daemon
- When the test suite runs
- Then
TestcontainersSmokeTestis reported as skipped- and the build is not failed by it
- and with a Docker daemon present the test still runs and verifies a container
Verified by
The Solution
A third BuildRunner by the same shell-out pattern as git and Docker.
BwrapBuildRunner shells out to the bwrap CLI (no library), unpacks a prepared Debian rootfs on demand into .git/werkator/buildenv/<envKey>/rootfs, reuses the step-16 git-metadata mounts verbatim, binds a persistent .git/werkator/buildenv/home as /root, and returns the attached bwrap process so streaming and cancellation match the other runtimes.
DispatchingBuildRunner routes by bwrap.enabled.
Pinning and mutual exclusion hold at the choke point.
bwrap.enabled and bwrap.rootfs join the pinned sandbox-policy set, stripped from the worktree layer so a branch cannot disable its sandbox or substitute a foreign rootfs.
docker and bwrap are mutually exclusive per build, rejected in buildSettings — the single point every build passes through — instead of picked silently.
Tooling makes the two manual steps reproducible.
tools/build-bwrap-rootfs.sh builds the rootfs archive (debootstrap-minbase Debian + JDK 21 + git + locales) on any machine with Docker, since debootstrap is not available on the target.
tools/werkator-build-prerequisites.sh re-runs the exact precondition command line from the plan on the target webspace and checks all three signals.
TestcontainersSmokeTest is gated with enabledIf docker available, so a Docker-less self-build skips it.
Open Questions
- Whether the rootfs archive built by
tools/build-bwrap-rootfs.shis complete for./gradlew buildhas not been exercised on a real webspace; the JDK 21, git and locales package set is a good baseline but project-specific tooling must be added. - The
systemduser unit for a webspace should gainMemoryMax/TasksMax(configurable per the plan); not implemented here.
Additional Changes
- Config template (
InitCommand),docs/configuration.mdandAGENTS.mdupdated so all three config places stay in sync and the pinned set is documented. - The plan file 17 records the precondition result and the new tooling.
Follow-up PRs
- ADR 0007 recording the bubblewrap runtime decision (options: bwrap vs proot/fakechroot vs plain native).
- Web-access deployment on a real webspace and the
docs/deployment.mdthird variant, written once verified there. MemoryMax/TasksMaxon the webspace systemd unit.