8.4 KiB
RFC 0001: Implementation Language for Werkdock
Status:
- proposed: 2026-09-01
- accepted: 2026-09-01
- rejected: -
Proposal: Werkdock is implemented in Go — as a single static binary, stdlib-only, with the sandbox engine behind an interface so bwrap can later be replaced by native namespaces.
Context and Problem Statement
Werkdock is a docker-like sandbox CLI over bwrap, filesystem isolation only (see README and Werkator plan step 21).
Three hard requirements drive the language choice:
- Distribution to a Managed Webspace without root — the tool must arrive and run with no package installation and no runtime dependency on the host.
- The work is process and filesystem orchestration — spawning
bwrap/tar/zstdwith streamed logs and forwarded signals, assembling mount arguments,doctorchecks. - Self-contained and testable — no code sharing and no build coupling with Werkator; the integration is
werkdock runas a CLI call, like git and docker.
Two further criteria matter in this project:
- AI-generated code quality — the tool is developed AI-assisted; languages where generated code is reliably correct and idiomatic reduce review load.
- Security — Werkdock assembles mount arguments and uid mappings from user input; language safety and a small supply chain count.
Considered Options
bash, Python 3, Kotlin Native, Rust, Go.
Scoring: −2 (unsuitable) to +2 (ideal), unweighted sum.
| Criterion | bash | Python 3 | Kotlin Native | Rust | Go |
|---|---|---|---|---|---|
| Distribution to webspace (no root) | +2 | +1 | −1 | +2 | +2 |
| Fit for process/FS orchestration | +1 | +2 | 0 | +2 | +2 |
| Testability | −2 | +2 | +1 | +2 | +2 |
| Robustness/maintainability as it grows | −2 | +1 | +1 | +2 | +2 |
| Closeness to the maintainer's stack (Kotlin dev) | 0 | +1 | +2 | −1 | +1 |
| Genre references to learn from | −1 | 0 | −1 | +1 | +2 |
| Future: own namespaces instead of bwrap | −2 | −1 | 0 | +2 | +1 |
| Toolchain/build effort | +2 | +2 | −2 | 0 | +2 |
| AI-generated code quality | −1 | +2 | 0 | +1 | +2 |
| Security | −2 | +1 | +1 | +2 | +2 |
| Sum | −5 | +11 | +1 | +13 | +18 |
The ranking is robust against re-weighting: Go scores below +1 in no criterion — it wins by absence of weaknesses, not by one outlier.
bash
Out on principle: the Werkator repository exists because a grown bash CI script became unmaintainable. A tool with subcommands, image/instance state, and doctor checks starts beyond the bash comfort zone. AI generates bash fluently but with the classic silent defects (quoting, word splitting, unchecked exit codes), and the missing test story means nobody notices. Security −2 is earned: injection via word splitting in exactly the kind of code Werkdock writes — user-supplied paths assembled into mount arguments. The existing scripts serve as specification, not as foundation.
Python 3
The best "no new compiler" candidate: present on every Debian webspace, the stdlib suffices (unpacking tar.zst shells out to zstd anyway), excellent testability, excellent AI generation.
Weaknesses: version drift across hosts (3.11/3.13), no static type check at runtime, and the tool runs as a tamperable source file on the host interpreter instead of as a binary.
Kotlin Native
Loses despite maximum stack closeness, and not narrowly — the weakness sits exactly where Werkdock lives:
- The stdlib gap hits the tool's core. Kotlin never had its own system libraries; on the JVM it delegates file, process, and IO work to the JDK. On Native that platform library is gone and only
platform.posixremains. Werkdock's central operation — spawning processes with log streaming, signal forwarding, and exit codes — means hand-writtenfork/execvp/waitpidover cinterop. - Kotlin Native was built for iOS, not for CLI tools. The driver was Kotlin Multiplatform (no JVM allowed on iPhone); the kotlinx ecosystem grew what mobile apps need. Mobile apps never spawn child processes, so no official process API exists.
- AI drifts to the JVM. The Kotlin training corpus is overwhelmingly JVM/Android; models reliably propose
ProcessBuilderandjava.nio, which do not exist on Native. - Distribution is build-machine-bound. Unlike the jlink bundle (which copies Temurin's prebuilt binaries, glibc floor 2.15, measured in Werkator ADR 0006), Kotlin Native compiles locally, so the binary's glibc floor is the build machine's.
- The expected payoff never materializes. There is no shared code and no shared build graph with Werkator by design; "same language" buys only developer familiarity — and JVM-library-free Native Kotlin feels more foreign than Go does after a week.
The honest variant of language consistency — Kotlin/JVM plus a jlink bundle like Werkator itself — was not on the ballot and would be disproportionate: a ~66 MB bundle for a sandbox helper copied to foreign webspaces, against one static Go binary.
Rust
Technically the strongest language for the genre and the best if Werkdock one day opens namespaces itself (direct syscalls, youki as a memory-safe sandbox reference).
Price: the steepest learning curve for a Kotlin developer and the slowest progress; AI-generated Rust needs iterations at the borrow checker, which the compiler at least enforces loudly.
Go
The sweet spot:
- The container world Werkdock imitates is written in Go — docker CLI, podman, runc — so every subproblem has a proven, readable reference.
- One static binary (
CGO_ENABLED=0) is the perfect webspace distribution; cross-compilation is aGOOS/GOARCHpair; builds take seconds. - Testing is built in;
gofmtknows exactly one style, which makes AI-generated Go above-average correct on the first attempt. - The stdlib covers everything the tool does (
os/exec,os,io,archive/tar), keeping the dependency list near zero — the smallest supply chain in the field. - Coming from Kotlin, Go is productive within days: garbage collector, familiar concepts, deliberately small language.
The Namespace Future, Concretely
Own namespaces instead of shelling out to bwrap are a real option, and Go keeps it open:
- The webspace kernel provably allows unprivileged user namespaces — Debian's
bwraphas not been setuid since bookworm and uses nothing else. - Go needs no cgo for it: namespaces are created when spawning the child via
SysProcAttr(Cloneflags,UidMappings/GidMappings), with the usual re-exec pattern (werkdock runstarts itself as a hidden init subcommand inside the fresh namespaces, sets up mounts, then execs the payload). - The concrete payoff: since kernel 5.11, overlayfs mounts are allowed inside a user namespace unprivileged — the webspace runs 6.1, but its
bubblewrap 0.8.0has no--overlay(added in 0.9.0). Own namespace code could provide the throwaway writable layer per build today. - The counterweight:
bwrapis hardened, Flatpak-tested code, and if the platform ever adopts an AppArmor userns restriction (as Ubuntu 24.04 did), the distribution'sbwrapwould likely stay permitted while a brought-along binary gets itsclone()refused.
Consequence for the design, independent of the engine question's outcome: the sandbox engine sits behind an interface from the start — engine 1 is bwrap (present, proven, invocation logic exists), engine 2 can later be native namespaces.
Concrete Proposal
- Language: Go, current stable toolchain, pinned in
go.mod(toolchaindirective). - Module:
werkdockas its own Go module in this subdirectory — no Gradle involvement,go build/go test/go vetare the whole toolchain. - Dependency policy: stdlib-only; any third-party dependency needs an RFC.
- Distribution: one static linux/amd64 binary, built with
CGO_ENABLED=0; other architectures are a build-matrix entry away if ever needed. - Style and quality gates:
gofmt(enforced),go vet, table-driven tests with the built-intestingpackage. - Architecture: CLI semantics (
run, images, instances,doctor) decoupled from a sandbox engine interface;bwrapis the first engine, native namespaces a possible second. - External processes:
bwrap,tar,zstdare called as CLIs viaos/exec— the same pattern Werkator uses for git and docker.
Decision Outcome
Accepted on 2026-09-01: Werkdock is implemented in Go, under the terms of the concrete proposal above.