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>
Die Link-Leiste neben dem Menü entfällt. An ihrer Stelle steht dort, wo
bisher der Repository-Name stand, eine Auswahlliste der bedienten
Repositories; werkator.js hängt die aktuelle Ansicht (branches, history,
current) an, damit der Wechsel auf derselben Seite bleibt. Mit nur einem
Repository bleibt der Name wie bisher reiner Text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Die offene Frage im PR-Dokument sagte „löst sich beim Merge". Das Log sagt
es genauer: Am Merge-Commit wurde `werkdock` noch eingereiht, erst am
nächsten nicht mehr. Die Projektschicht ist die Datei im Arbeitsbaum des
beobachteten Klons — die Definition überlebt, bis dieser Baum an der
Entfernung vorbei ist, der Merge selbst bekommt also einen letzten
Geister-Build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Geschrieben wurde es als #14 — vergeben wurde beim Öffnen #15. Die
Namenskonvention (docs/prs/README.md) will die Nummer aus dem Tracker,
sonst zeigt später nichts mehr aufeinander.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor: werkdock/ verlässt dieses Repository
Die neun Commits unterhalb von werkdock/ sind per `git subtree split`
historieerhaltend nach https://git.javagil.de/mi/werkdock gehoben; hier
bleibt nichts davon zurück.
Mit dem Verzeichnis geht die Build-Definition `werkdock`: Sie beschrieb ein
Go-Modul, das der Kotlin-Build um sie herum nicht berühren kann — jeder
Werkator-Branch baute es dennoch als `<branch>@werkdock` mit. Drüben ist
sie die einzige und heißt `default`.
Die Kopplung bleibt, wie Sitzung C sie gemacht hat: Werkator ruft das
`werkdock`-Binary über PATH (`bwrap.werkdock`, weiterhin gepinnt), so wie
es `git` ruft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(remote): das werkdock-Binary kommt aus einem eigenen Checkout
Bisher baute das Skript es aus dem Unterverzeichnis. Jetzt nennt
WERKDOCK_REPO den Checkout (Vorgabe: ein Geschwister dieses Repositories —
die übliche Ablage, wenn man an beidem arbeitet), WERKDOCK_BINARY das
gebaute Binary darin; beides ist wie jeder Transportwert aus der Umgebung
übersteuerbar.
Fehlt der Checkout, nennt die Meldung die Klon-URL, statt bloß „file not
found" zu sagen: Das Skript kann die Lage nicht mehr selbst beheben, also
sagt es, was sie beheben würde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: Sitzung E — Werkdock hat ein eigenes Repository
Plan-Schritt 21 bekommt die Sitzung, die die Herauslösung beschreibt, samt
Abnahmekriterium; Plan-Index und Architektur-Skill sagen nicht mehr, sie
stehe noch bevor, und der Deployment-Leitfaden verlinkt das Repository
statt eines Verzeichnisses, das es hier nicht mehr gibt.
Dazu das PR-Dokument nach Hausregel (docs/prs/README.md).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(remote): repo-add wendet das Instanz-Fragment an
`repo-add` rief `werkator init` ohne `--apply`, `repo-init` dagegen mit.
Folge: Ein so aufgenommenes Repository bekam die Sandbox-Politik nicht —
`bwrap.rootfs` und `bwrap.werkdock` fehlten, und der erste Build lief auf
dem nackten Host statt in der Sandbox. Gefunden beim Aufnehmen von
Werkdock: `gofmt: command not found`, weil die Go-Toolchain nur im
Sandbox-Image liegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(remote): repo-add erkennt einen ~/-Registry-Eintrag, und der Kommentar sagt die Wahrheit
Die Registry-Prüfung suchte nur den absoluten Pfad und meldete deshalb
„not registered yet“, obwohl der Eintrag als `path: ~/werkdock` dasteht —
eine Einladung, ihn ein zweites Mal einzutragen. Gegriffen wird jetzt
lokal, damit beide Schreibweisen ohne zweite Schicht Remote-Quoting
treffbar sind.
Dazu der Kommentar über dem Laden der Env-Datei: Er behauptete, die
ausdrückliche Umgebung gewinne. `set -a; source` weist unbedingt zu — die
Datei gewinnt. Beim Nachziehen des Werkdock-Fragments hat mich genau das
eine Runde gekostet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`tools/remote` konnte genau ein beobachtetes Repository einrichten
(`repo-init`, fest auf $WERKATOR_PATH/werkator). Für eine Instanz, die eine
Registry bedient (ADR 0009), fehlte der Weg, ein zweites hinzuzunehmen —
und das braucht der Rollout unabhängig davon, welche Repositories es am Ende
sind.
`repo-add <url> [name]` klont daneben, führt `init` darin aus und PRÜFT, ob
die Registry den Pfad schon nennt — geschrieben wird sie nicht. `~/.werkator.yml`
ist die Datei der Instanz: Port, globale Nebenläufigkeit, womöglich geteilte
Zugangsdaten. Ein Skript, das sie in place umschreibt, verändert die
Konfiguration des Betreibers hinter dessen Rücken. Klonen und Initialisieren
ist mechanisch, Registrieren ist eine Entscheidung — also druckt es den
Eintrag und sagt, dass die Registry beim Start gelesen wird.
Der Name muss ein Pfadsegment sein: Er wird das Routen-Segment /repos/<name>.
docs/deployment.md verweist im Registry-Abschnitt darauf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der letzte offene Punkt der Sitzung D („Gitea status links use the repo-scoped
URLs"). Beim Nachsehen war es kein Scoping-Problem, sondern ein fehlendes
Feature: `server.publicBaseUrl` ist als „used for all links posted to Gitea"
dokumentiert, der Executor postete aber `targetUrl = null` — es gab überhaupt
keinen Link. Jetzt zeigt der Status auf die Artefaktseite des Builds, also auf
die Logs, um die es geht. Ohne `publicBaseUrl` bleibt er null: ein relativer
Link in einem Gitea-Status wäre schlimmer als keiner, er löste gegen die Forge
auf.
Die Präfix-Regel wohnt jetzt an einer Stelle (`RepoLinks`), weil drei
dasselbe damit tun: die Seiten, die API-Antworten und diese Ziel-URLs — und
eine dreimal ausgeschriebene Regel driftet. Die Slice-Tests binden die echte
Komponente per @Import ein, statt die Regel im Test nachzubauen.
Ein neuer Test (mit publicBaseUrl trägt der Status die Artefaktseite; die
vorhandenen Tests halten fest, dass ohne sie weiterhin null gepostet wird).
499 Tests grün, ktlint sauber. Plan und Architektur-Skill nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der aus Sitzung C nach E übertragene Doku-Punkt: docs/deployment.md
beschreibt die Registry jetzt betrieblich — klonen, vorbereiten, in
~/.werkator.yml eintragen, neu starten. Dazu die Regeln, die beim Eintragen
weh tun, wenn man sie nicht kennt: Der Name ist Routen-Segment und
Umschalter-Eintrag und muss eindeutig sein (ein Doppel bricht den Start ab,
ebenso ein Eintrag, der kein git-Repository ist), eine unlesbare Konfiguration
überspringt nur ihr Repository, geteilte Zugangsdaten können einmal im
defaults-Block liegen, und die Unit behält ihren Namen — sie gehört der
Instanz, nicht einem Repository.
Dazu, was die Adressen tun: mit einem Repository bleibt jede URL, wie sie
war; mit mehreren tragen Seiten und API das Repository, und die unscoped
Pfade meinen weiterhin das bediente.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zweite Hälfte von Sitzung D (docs/plan/22-multi-repo.md, PR #13): Der Server
bediente bisher genau ein Repository der Registry. Jede Route — API, Seiten,
Artefakt-Dateien — arbeitete auf `registry.current()`; ein zweites
registriertes Repository wurde gebaut und gepollt, war aber unsichtbar und
unerreichbar.
Jeder Controller löst sein Repository jetzt je Anfrage auf, statt das
bediente als Bohne zu halten; jede Route ist zweimal gemappt. Die unscoped
Form ist kein Übergangs-Alias, sondern dauerhaft die Art zu sagen „das
bediente Repository" — Lesezeichen und die nach Gitea geposteten Links
kennen kein Segment.
Entschieden gegen die Repo-Spalte: Die Seiten bleiben je Repository, die
Navigation bekommt einen Umschalter. Die Aktionen einer Zeile brauchen das
Repository ohnehin, Branches kommen von einem origin und Artefakte aus einem
Store — und bei dem einen Repository, das die meisten Installationen haben,
wäre eine Spalte nur Rauschen.
Das Link-Präfix folgt der ZAHL der bedienten Repositories, nicht dem Weg, über
den eine Seite erreicht wurde: mit einem behält die Installation ihre
bisherigen URLs (Abnahmekriterium der Sitzung), mit mehreren benennt jeder
Link sein Repository. werkator.js liest das Präfix einmal aus einem
`werkator-repo-base`-Meta. `BranchPermalinks.permanentUrl` bekommt es
ebenfalls — der permanente Schlüssel ist ein Hash des Build-Namens allein,
zwei Repositories mit `main` teilten sich sonst eine permanente URL.
Fünf neue Tests, Gegenprobe per Mutation gezogen (Präfix fest auf leer →
genau der Mehr-Repo-Test fällt). 498 Tests grün, ktlint sauber. PR-Dokument
docs/prs/2026-09-03-PR#13-…, Plan, Architektur-Skill und AGENTS.md
nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der letzte Übertrag aus Sitzung C: `RunningBuild` trug kein Repository, der
Executor ist aber instanzweit. Zwei Folgen, beide gemessen und jetzt behoben:
- Das Worktree-Aufräumen schützte die Worktrees ALLER Repositories. Ein
laufender Build eines anderen Repositories auf einem gleichnamigen Branch
hielt hier einen Worktree fest, dessen Branch längst von origin weg war.
- Die Current-Builds-Ansicht und `/api/builds/current` zeigten die Builds
aller Repositories, schlugen ihren Status aber in den Ergebnissen NUR
dieses Repositories nach — ein fremder Build fiel auf RUNNING zurück und
zeigte einen Zustand, den niemand aufgezeichnet hat. Dasselbe beim
Live-Log: der Schlüssel eines fremden Builds wurde beantwortet.
`RunningBuild.repo` ist der `RepoContext` selbst, verglichen wird per
Referenz — der Kontext ist die Identität (ADR 0009), und der Executor hat ihn
in `startBuild` ohnehin zur Hand. Watcher, API und UI filtern damit auf ihr
eigenes Repository.
Drei neue Tests, Gegenprobe per Mutation gezogen: Nimmt man die drei Filter
wieder heraus, fallen genau diese drei und sonst keiner. 493 Tests grün,
ktlint sauber.
Architektur-Skill und Plan (docs/plan/22-multi-repo.md) nachgezogen — die
Aussage „RunningBuild carries no repository" stimmte nicht mehr. Was von
Sitzung D bleibt: die repo-bezogenen Routen und die Oberfläche, die weiterhin
nur `registry.current()` bedienen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Datei lag als `PR#000`-Platzhalter, wie es die Konvention in
docs/prs/README.md vorsieht: Die Nummer gibt es erst, wenn der Pull Request
offen ist. Er ist es — #12. Umbenannt samt der fünf Szenario-IDs
(`Scenario#000.01` … `.05` -> `Scenario#12.01` … `.05`), wie es Schritt 21
für PR #4 vorgemacht hat; sonst zeigen die IDs auf eine Nummer, die es nicht
gibt. Kein `#000` bleibt in der Datei, keine andere Datei verweist auf den
alten Namen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The metrics page sums the registered repositories' sizes; the instance
configuration is documented in docs/configuration.md, the architecture skill,
AGENTS.md and the step 22 plan (session C ticked, fairness decided FIFO).
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>
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>
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>
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>
- 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)