Die alte GitHub-PR#14 kollidiert mit der Gitea-Nummerierung, die nach dem
Umzug bei 1 neu beginnt; PR#16 ist die tatsächliche Nummer auf git.javagil.de.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Die Volume-Zahlen sind nicht mehr nur Rückfall, sondern dritter Kandidat: Gebunden
ist, was am wenigsten Platz lässt; eine später eingeführte User-Quota geht ohne
Änderung in den Vergleich ein.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Auf mih09 zeigt /system die 71-GiB-Platte des Hosts, gemessen bindet aber die
Gruppen-Quota des Pakets (8 GiB soft, 12 GiB hard, 1,04 GiB belegt). Das PR-Doc
plant: quota(1) per CLI lesen, die bindende Zeile (User/Gruppe, Dateisystem des
Repos) als Disk-Budget nehmen, ohne Quota unverändert der File Store, Serien-Reset
bei Quellenwechsel, Budget in der Info-Zeile benannt. Nur Plan, kein Code.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: RepoContext over the current repository
A RepoContext bundles a repository's primary checkout with the state that lives
inside or is keyed by it (results, artifact store) and carries its name. Today
there is exactly one, opened over the current working directory; the result and
artifact-store beans now come from it, so nothing else changes yet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: the watcher polls a RepoContext
start/poll/recoverOnStartup take the context instead of a working directory and
read results and artifacts from it; the per-repository poll memory (logged fetch
error, deprecation warning, cached branch definitions) moves into a RepoWatch
keyed by context, so the next session can iterate contexts without one
repository's outage silencing another's. The shared WatcherState is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: the executor runs builds of a RepoContext
startBuild takes the context first; builds serialize per (context, branch) and
share the global maxConcurrent cap across repositories, results and artifacts go
to the build's own context. ConsoleBuildRunner, the build/retry commands and the
builds API restart pass the current repository's context along.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: the UI and the branch listing read their RepoContext
UiController and BuildsApiController take the current repository's context
instead of a settable working directory; BranchListing lists the branches of a
context and reads the results from it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Step 22 B: document the RepoContext, PR-doc for PR #11
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
ADR 0009 and step 22 plan: one Werkator instance serves a set of repositories
# Conflicts:
# docs/plan/22-multi-repo.md
# docs/prs/2026-09-01-PR#6-werkdock-bootstrap.md
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>
'werkator init --apply FILE' installs a config-schema YAML fragment as
its own layer: validated strictly (an unknown key is refused loudly,
never ignored — a typo must not install a silent no-op), then copied
verbatim to .git/werkator/.werkator.applied.yml, above the project
config and below the hand-edited machine config, which always wins.
Deviation from the plan sketch, recorded there: a separate layer
instead of an in-place merge, because merging would re-serialize the
machine config — destroying its comments and rewriting the file that
holds the secrets; re-apply is a plain file replacement.
init --systemd now also generates werkator.htaccess beside the units
whenever a publicBaseUrl is configured — generated host integration for
the managed-webspace Apache, copied into the docroot by the wrapper.
New subcommand 'werkator control-token' prints (and lazily creates) the
token via ControlTokenService, so no wrapper needs its own generator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Refinement decided 2026-09-01: each side gets its native format — the
wrapper keeps a bash-sourceable transport env file, Werkator takes a
YAML fragment in its own config schema via 'init --apply FILE',
deep-merged idempotently. The env-to-config mapping table disappears
entirely: the fragment IS configuration in the one schema, validated by
the existing binding, documented by the existing reference. The env
file names the fragment (WERKATOR_INIT_CONFIG), keeping one entry point
per instance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decided 2026-09-01: Werkator becomes the executing app, the remote
script a thin wrapper (build, transport, remote execution, service
switching). Parameters travel as env files instead of many options —
'remote --env .env.mih34 werkator ...' selects the target, and
'werkator --env ... init' reads the same file and writes real values
into the files it owns, idempotently; the wrapper's YAML heredocs and
sed patches (the duplicate-block class) die. control-token becomes a
werkator subcommand, check-prerequisites delegates to werkdock doctor.
Explicitly not the removed legacy env-to-YAML conversion: the env file
is setup-time input to init, never runtime configuration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/remote reworked to role-named commands (the builder-vs-built
tangle noted 2026-09-01): instance-install/instance-update/
instance-start manage the installed Werkator + werkdock, repo-init the
watched repository. The self-build is retired — no repo clone for
building, no GitHub-key step; the instance installs from locally built
artifacts (ADR 0006), the watched repo clones anonymously via https.
instance-update refuses to swap the runtime under a running build
(FORCE=1 overrides) and keeps the previous runtime as the rollback
asset. repo-init is idempotent: checksum-skipped rootfs upload, and the
machine-config guard is fixed — its indentation mismatch (grep for two
spaces, block written with four) appended the bwrap block on every run,
the source of the nine duplicates found on mih34. The retired
install/build/start commands fail loudly naming their successors.
docs/deployment.md gains the Hostsharing Managed Webspace as the third
deployment variant, written from the live-verified setup: both new
commands ran against mih34 (full update cycle; fully skipping re-init).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runner no longer assembles raw bwrap argv: it checks image
existence via 'werkdock images', loads the rootfs archive once per
source as image werkator-buildenv-<hash> into werkdock's store (shared
across every repository of the OS user — resolving step 22's
buildenv-sharing question), and runs builds through 'werkdock run --rm'
with the git-metadata mask expressed as ordered -v/--tmpfs flags.
New pinned config key bwrap.werkdock (the executing binary, default via
PATH) — a branch must not substitute the executing binary; synced in
WerkatorConfig, BwrapOverrides, the pinned-key strip, the init
template, docs/configuration.md, and AGENTS.md.
werkdock's --clearenv means the server environment no longer leaks into
builds; the TMPDIR workaround is gone with the raw invocation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The image-name pattern allows dots, so a directory like broken.tmp —
an interrupted load's staging dir — counted as an image. List now skips
.tmp names and Load refuses them, closing the ambiguity the previous
commit's (accidentally pushed red) test exposed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session C groundwork: Werkator's git-metadata mask needs a tmpfs
BETWEEN binds (ro-bind .git, tmpfs .git/werkator, bind workspace), so
-v and --tmpfs now collect into one ordered mount list and RunSpec
carries Mounts instead of Binds. --tmpfs DEST is the docker flag of the
same name. `werkdock images` lists loaded image names one per line, so
a consumer can check existence through the CLI. -v accepts the explicit
:rw docker default instead of refusing it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the tenet revision and its shape: instance registry and
instance keys in ~/.werkator.yml (one instance per OS user), optional
repo defaults in an explicit defaults: block merged below every repo's
own layers, repo config unchanged in each repository, registry-wins
precedence, basename repo names. Rejected: a federation dashboard over
single-repo instances (keeps N services), and the status quo.
Konzept/AGENTS architecture wording changes only when the
implementation lands; the decision list carries 0009 now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repo defaults allowed in the home file, but only inside an explicit
defaults: block, merged below every repo's own layers (accepted cost:
secrets may live in two places). Registry wins over cwd when a home
config exists. Repo names default to the directory basename,
overridable per registry entry, duplicates abort loudly. The file name
stays .werkator.yml in all three locations — the location carries the
meaning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Host/instance configuration (domain, port, registry, global concurrency)
lives in a .werkator.yml in the home directory of the user running the
instance — one instance per OS user, matching the platform model; repo
configuration stays in each repository's .git/werkator/. Repo-level
instance keys get ignored with a warning once a home config exists.
The open questions now name the decisions still needed: repo defaults
in the home file, cwd-vs-registry precedence, repo naming, and whether
the third .werkator.yml location should carry a distinct name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-instance-per-repository tenet does not scale to a second
repository on a webspace (second service, port, tunnel, UI). Step 22
plans the revision in five sessions: ADR 0009 + instance registry and
key-ownership split, a RepoContext refactor with unchanged behavior,
watcher/executor multiplexing with per-repo error isolation, repo-scoped
routes and UI with single-repo back-compat, and the mih34 rollout with
Werkbaum as the second repository. Guiding idea: the repository stays
self-contained (state in its own .git/werkator), the instance is only
an aggregator — adding a repo is a registry entry, never a migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
openjdk-21-jdk-headless instead of the full JDK (drops the X11 and
fontconfig library stack; headless AWT still works for tests),
message catalogs except en/de removed, man pages, package docs, apt
lists and cache cleared after install.
Measured on the JDK+Go+Node image: 516 MB -> 351 MB compressed —
smaller than the original JDK-only archive (375 MB).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/remote mixes commands that manage the builder (the installed
Werkator instance) with commands that act on the built (the watched
repository); on the self-building instance both roles coincide in one
product, which misleads. Session D's replacement must name the role in
every command and delegate built-side operations to the werkator CLI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every artifactDir landed below reports/ — the legacy
archived_artefact_dir_path layout — which mislabeled non-report outputs
(reports/werkdock/dist/werkdock) AND hid them: the artifact page's
report index only scans reports/ for HTML pages, so a built binary was
stored but never shown.
Now build/reports keeps archiving as reports/ (the browsable anchor and
every existing link), every other directory archives at its
workspace-relative path, and the artifact page gains a plain-files list
for everything outside reports/ (log files stay in their own section;
capped at 200 entries). Existing stored artifacts keep their old layout
and remain served; only new builds use the new one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bwrap inherits the server's environment; on hosts with pam_tmpdir that
includes TMPDIR=/tmp/user/<uid>, which does not exist on the sandbox's
fresh tmpfs /tmp. Every tool honoring TMPDIR then fails — seen live on
mih34 as go's 'creating work dir: stat /tmp/user/120957: no such file
or directory'. The JVM ignores TMPDIR, so Gradle builds never noticed.
Explicit environment and bwrap.env entries can still override.
(Werkdock's own engine is immune by design: it runs --clearenv.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--exclude=sys matched every path component named sys anywhere in the
tree — GNU tar applies slash-less patterns to all components — and so
silently dropped usr/share/go-1.24/src/internal/runtime/sys from the
archive, breaking every go build in the sandbox with 'package
internal/runtime/sys is not in std'. The excludes are now anchored
(./proc, ./sys, ./dev), so only the top-level mountpoints stay out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run-time composition instead of baked-in toolchain combinations: a slim
base image plus per-toolchain prefix binds (--with jdk-21 --with go-1.24),
possible because official toolchain tarballs live under their own
prefixes — plain ro-binds, no overlayfs, no root, today's bwrap.
Comes due when a second toolchain combination is needed; until then the
one fat image stays the deliberate choice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flat images without layers (~2 GiB unpacked for a fat JDK+Go+Node
buildenv), near-free instances (read-only rootfs bind + tmpfs), the
one-fat-image recommendation, the orphaned-environment trap on archive
path changes, and the future overlay/hardlink-dedup options.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A second named build beside the default: gofmt gate, go vet, go test,
and the static binary as the artifact (werkdock/dist). Verified locally
end to end and via config:print.
Expected red on the webspace instance until the trixie rootfs learns
the go toolchain — that image extension (Go + Node, roadmap step 2 of
plan 21) is the follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The minimal build-capable CLI decided in RFC 0002's outcome: stdlib-only
Go module, one static binary.
- engine: RunSpec behind the Engine interface (RFC 0001); the Bwrap
engine ports Werkator's hardened invocation — uid-0 mapping, read-only
rootfs at /, proc/dev/tmp/root before the user binds so binds below
them land inside, mountpoint pre-creation in the rootfs including file
mountpoints, and a guard against binds escaping the rootfs. --clearenv
gives docker-style clean environments (HOME/PATH set explicitly).
- store: images under $WERKDOCK_HOME (default ~/.werkdock), load
unpacks via the tar CLI into a tmp dir and renames atomically.
- cli: docker-shaped run flags (-v/-e/-w/--rm); refused docker flags
(-p, --network, --memory, --cpus, --user, -d) fail loudly with the
reason; exit codes follow docker (125 CLI errors, child code through).
- doctor: port of werkator-build-prerequisites.sh — userns probe with
the three signals, tar/zstd, free space and group-quota headroom via
testable df/quota parsers, same PASS/FAIL output.
- tests: argv golden test, mountpoint and escape tests, flag refusals,
store round trip, doctor parsers — plus real-sandbox integration
tests that skip where bwrap or userns are unavailable.
Also records in step 21: RFC 0002 levels 2/3 deferred; next goal is
sandbox builds of Werkator, Werkbaum, and Werkdock itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Level 1: docker-compatible CLI (verbs, flags, loud refusal of isolation
flags the filesystem-only contract cannot honor) — built in session B.
Level 2: OCI image pull, flattened to a rootfs — deferred, stdlib-doable.
Level 3: a daemon speaking the Docker Engine API subset Testcontainers
actually uses (Testcontainers never calls the CLI) — deferred, but the
CLI is built as a thin frontend over the same internal service from the
start. Records the port-mapping crux of host networking and the
unprivileged-netns escape hatch as a future RFC.
Step 21 session B and the werkdock README follow the docker-shaped
semantics: run takes an image, instances correspond to containers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full five-way evaluation (bash, Python 3, Kotlin Native, Rust, Go)
with the numeric scoring, the Kotlin Native deep-dive, and the
own-namespaces-instead-of-bwrap analysis, ending in a concrete proposal:
Go, stdlib-only, one static binary, sandbox engine behind an interface
so bwrap can later be swapped for native namespaces.
Status proposed — the decision outcome is recorded once made.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 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>
Records where the bwrap work (PR #4) drifted from the intent of ADR 0006 —
the webspace self-build instead of local-build-plus-install — and breaks the
correction into four sessions: close step 17's open ends, bootstrap Werkdock,
let Werkator consume it, replace the self-build with the bundle install path.
The extracted sandbox tool is named Werkdock (decided after three naming
rounds, rationale and dropped candidates in the step file). It grows in the
werkdock/ subdirectory, seeded here with its README, and moves to its own
repository once it stands on its own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add the bubblewrap build runtime (step 17, ADR 0007)
BwrapBuildRunner: third runtime behind BuildRunner for hosts without root
and without Docker (e.g. Hostsharing managed webspaces). Shells out to the
bwrap CLI, unpacks a prepared rootfs on demand into
.git/werkator/buildenv/<envKey>/rootfs, reuses the Docker runner's git
metadata mounts, and returns the attached bwrap process for streaming and
cancellation.
Config: bwrap.enabled/rootfs/env on BranchConfig and BwrapOverrides on
BuildDefinition; enabled/rootfs are pinned like the docker sandbox policy.
Docker and bwrap are mutually exclusive per build, rejected in
buildSettings instead of picked silently. DispatchingBuildRunner routes
bwrap; InitCommand template, docs/configuration.md and AGENTS.md in sync.
* bwrap rollout tooling: remote script, prerequisites disk/quota check, absolute workspace binds
- tools/remote: central remote control script with check-prerequisites, install and build commands
- tools/werkator-build-prerequisites.sh: compact PASS/FAIL output, target-dir parameter, free-space and
group-quota headroom checks against the ~5 GiB build footprint, home-filesystem reference
- BwrapBuildRunner: bind workspace and home at absolute paths resolved against repoDir — a relative
path made bwrap create mountpoints inside the read-only rootfs (seen on the webspace); regression test
- TestcontainersSmokeTest: gated with enabledIf docker available (skip, never fail, without a daemon)
- docs: configuration reference, step-17 plan notes, PR-doc
* bwrap: bind the repo read-write before the workspace so mountpoints are creatable
bwrap creates mountpoints for bind destinations inside the sandbox; with only a
read-only rootfs bound at /, creating them for the workspace under .git/werkator/
worktrees failed with 'Read-only file system' (seen on the webspace). Binding the
repo dir read-write first provides the base; the git metadata mounts then layer
the usual isolation on top (read-only .git, tmpfs mask over .git/werkator,
read-write worktree admin dir).
* bwrap: pre-create bind mountpoints inside the unpacked rootfs
bwrap mkdirs mountpoints for bind destinations against the sandbox view; with the
rootfs ro-bound at / every destination missing from the rootfs (the repo dir under
/home/storage/... on the webspace) fails with 'Read-only file system'. The rootfs
directory is a plain host dir, so create the mountpoints there before launching
bwrap; it then finds them and has nothing left to create.
* bwrap: skip existing rootfs files when pre-creating bind mountpoints
/etc/resolv.conf is a file the rootfs already ships; createDirectories threw on it.
Only missing directories are created now.
* bwrap: pre-create proc/dev/tmpfs mountpoints in the rootfs too
The rootfs archive ships no /proc or /dev (excluded when packed), so bwrap failed
mkdir'ing their mountpoints against the read-only root.
* bwrap: bind the workspace after the git metadata mounts
The tmpfs mask over .git/werkator shadowed the earlier workspace bind, because the
worktree lives under .git/werkator/worktrees — chdir then failed with ENOENT. The
workspace bind now comes last and shadows the mask at exactly its own path.
* systemd resource limits and webspace start command (step 17, web access)
- server.systemd.memoryMax/tasksMax (empty = directive omitted): on platforms where
the service runs in a shared memory slice (Hostsharing Managed Webspaces) a
runaway Gradle build must not starve the whole package; init --systemd reads the
effective config and bakes the values into the generated unit
- tools/remote werkator start: writes server settings (assigned port, loopback
bind, publicBaseUrl, nginx off) plus the Apache reverse-proxy .htaccess into
~/doms/<domain>/subs/www, runs init --systemd and enables the user unit
- docs/configuration.md documents the new keys
* tools/remote: env-based configuration and background port-forward
All connection and deployment values come from .env in the repository root
(WERKATOR_REMOTE, WERKATOR_PATH, WERKATOR_PORT, WERKATOR_DOMAIN,
WERKATOR_LOCAL_PORT, optional WERKATOR_BRANCH/MEMORY_MAX/TASKS_MAX/ROOTFS);
missing values fail with a pointing error instead of positional parameters.
- port-forward is now 'tools/remote port-forward start|stop' with a detached
ssh tunnel, pid file under /tmp, and idempotent start
- start restarts the systemd unit after updating the machine config
- control-token generates the token in place when the server has not yet
- the rootfs archive default moves to build/ (already gitignored)