A Dockerfile builds on the target, and two backends for one interface (0.12.0) #14

Merged
mi merged 7 commits from feature/dockerfile-builds into main 2026-10-20 15:41:48 +02:00
Owner

RFC 0004 had ruled build out as "not planned: needs writable rootfs snapshots per step and pull for FROM". Both halves expired: pull arrived in 0.3.0, and per-step snapshots are what a build cache needs, not what a build needs — one writable tree over the base is enough.

RFC 0006

docs/rfcs/0006-dockerfile-builds.md decides the build model, cuts the one version-dependent interface, and argues the "no layers" question from measurements rather than from the README's shorthand.

Measured on a debian:trixie rootfs with bwrap 0.11.1 and a 0 <uid> 1 map — every number in the RFC was taken, not reasoned about:

  • A writable root bind (--bind instead of --ro-bind) works unprivileged.
  • CapEff is 0000000000000000: mount -t tmpfs and mknod are both EPERM, so the sandbox can mount nothing itself and every mount must stand in bwrap's argv. That is what "no layers" was ever about.
  • Plain apt-get update dies on apt's _apt user (seteuid 42 failed, Method http has died unexpectedly); APT::Sandbox::User root fixes it.
  • apt-get install nginx still fails (chown: changing ownership of '/var/log/nginx': Invalid argument, dpkg exit 100). fakeroot alone does not help — libfakeroot is demonstrably active and its chown wrapper still reports the real syscall's EINVAL. fakeroot plus FAKEROOTDONTTRYCHOWN=1 does.
  • Overlay works unprivileged on bwrap 0.11, --overlay-src stacks, and the upper directory is already a layer diff: changed files as themselves, deletions as character devices 0:0 — what OCI writes as .wh.NAME. Two snags: the workdir's work/ is left at d---------, and bwrap refuses overlay when installed setuid.
  • With a writable root, bwrap creates a missing bind mountpoint itself and it stays in the tree — so a build binds only /etc/resolv.conf, which base images already have.

The interface (the point of the RFC)

build.Backend / build.Tree / build.Step. The boundary is exactly "how does a writable tree over a base come into being, and how is its result harvested"; no caller ever sets a RunSpec's root fields. Everything else — parser, instructions, COPY, multi-stage, CLI, uid levers, commit — is identical in both worlds.

  • Copy backend (implemented): a real full copy of the base, root bound read-write, commit is the rename, no cache. It stays after the Debian 13 upgrade, as the fallback doctor picks when bwrap is too old or setuid, and for single-shot builds.
  • Overlay backend (deliberately not implemented): designed, with the measurements above as the evidence that it fits. The per-step cache waits for it, because without a union mount a cached step costs a whole tree copy.

The warning that belongs with this, in the RFC and as a comment in the code: store.copyTree hardlinks so a second tag costs no data — there both copies are read-only. For a build tree it is unusable, because a RUN step writing in place through such a hardlink would modify the base image.

Stage 1, implemented

werkdock build [-f Dockerfile] -t IMAGE CONTEXT with docker's flags. FROM, RUN, COPY plus the metadata instructions import -c already knew, through the same store.ApplyChange. Staging and commit reuse the mechanics import has had since 0.11.0 (<image>.tmp, the per-image flock, rename), now exported as store.Staging.

Werkdock sets both uid levers itself so the Dockerfile stays portable; the base image needs fakeroot installed, and build says so up front when it has not. engine.RunSpec gained WritableRoot, and a writable-root sandbox gets no implicit tmpfs over /tmp, /root, /run — with one, what a RUN step writes there would vanish in the next step while docker keeps it in the image.

Refused loudly rather than ignored: EXPOSE, HEALTHCHECK, STOPSIGNAL, ONBUILD, MAINTAINER at parse time; ARG, SHELL, ADD and multi-stage named as stages 2 and 3; BuildKit's RUN --mount with the CapEff measurement as the reason; and the dozen docker build flags werkdock cannot keep the promise of.

Verified

  • go test ./..., go vet ./..., gofmt -l . — green and empty.
  • A real sandbox integration test builds FROM/WORKDIR/COPY/RUN/CMD and then runs the result; it skips without bwrap/userns like the existing ones.
  • The apt path, which the levers exist for, is a second integration test pointed at a Debian base (WERKDOCK_TEST_BASE_ROOTFS). Run here: nginx-common configures, dpkg-query in the built image reports install ok installed, the apt fragment is gone from the result.
  • End to end through the CLI: import a 143 MB debian:trixie base with fakeroot, build -t probe:1 with apt-get install -y nginx jq in the Dockerfile — 6.8 s — then run --rm probe:1 prints what its RUN steps and its COPY produced.

Follow-ups the RFC names without deciding: store dedup between image versions, keeping layer blobs as a transport and cache format (the one with the highest running cost today), doctor IMAGE, and ADD from a URL.

🤖 Generated with Claude Code

RFC 0004 had ruled `build` out as "not planned: needs writable rootfs snapshots per step and `pull` for FROM". Both halves expired: `pull` arrived in 0.3.0, and per-step snapshots are what a build *cache* needs, not what a build needs — one writable tree over the base is enough. ## RFC 0006 [docs/rfcs/0006-dockerfile-builds.md](docs/rfcs/0006-dockerfile-builds.md) decides the build model, cuts the one version-dependent interface, and argues the "no layers" question from measurements rather than from the README's shorthand. Measured on a `debian:trixie` rootfs with bwrap 0.11.1 and a `0 <uid> 1` map — every number in the RFC was taken, not reasoned about: - A writable root bind (`--bind` instead of `--ro-bind`) works unprivileged. - `CapEff` is `0000000000000000`: `mount -t tmpfs` and `mknod` are both EPERM, so the sandbox can mount nothing itself and every mount must stand in bwrap's argv. **That** is what "no layers" was ever about. - Plain `apt-get update` dies on apt's `_apt` user (`seteuid 42 failed`, `Method http has died unexpectedly`); `APT::Sandbox::User root` fixes it. - `apt-get install nginx` still fails (`chown: changing ownership of '/var/log/nginx': Invalid argument`, dpkg exit 100). `fakeroot` **alone does not help** — libfakeroot is demonstrably active and its `chown` wrapper still reports the real syscall's EINVAL. `fakeroot` plus `FAKEROOTDONTTRYCHOWN=1` does. - Overlay works unprivileged on bwrap 0.11, `--overlay-src` stacks, and the upper directory is already a layer diff: changed files as themselves, deletions as character devices 0:0 — what OCI writes as `.wh.NAME`. Two snags: the workdir's `work/` is left at `d---------`, and bwrap refuses overlay when installed setuid. - With a writable root, bwrap creates a missing bind mountpoint itself and it stays in the tree — so a build binds only `/etc/resolv.conf`, which base images already have. ## The interface (the point of the RFC) `build.Backend` / `build.Tree` / `build.Step`. The boundary is exactly "how does a writable tree over a base come into being, and how is its result harvested"; no caller ever sets a `RunSpec`'s root fields. Everything else — parser, instructions, COPY, multi-stage, CLI, uid levers, commit — is identical in both worlds. - **Copy backend** (implemented): a real full copy of the base, root bound read-write, commit is the `rename`, no cache. It stays after the Debian 13 upgrade, as the fallback `doctor` picks when bwrap is too old or setuid, and for single-shot builds. - **Overlay backend** (deliberately *not* implemented): designed, with the measurements above as the evidence that it fits. The per-step cache waits for it, because without a union mount a cached step costs a whole tree copy. The warning that belongs with this, in the RFC and as a comment in the code: `store.copyTree` hardlinks so a second tag costs no data — there both copies are read-only. For a build tree it is unusable, because a `RUN` step writing in place through such a hardlink would modify the base image. ## Stage 1, implemented `werkdock build [-f Dockerfile] -t IMAGE CONTEXT` with docker's flags. `FROM`, `RUN`, `COPY` plus the metadata instructions `import -c` already knew, through the same `store.ApplyChange`. Staging and commit reuse the mechanics `import` has had since 0.11.0 (`<image>.tmp`, the per-image `flock`, `rename`), now exported as `store.Staging`. Werkdock sets both uid levers itself so the Dockerfile stays portable; the base image needs `fakeroot` installed, and `build` says so up front when it has not. `engine.RunSpec` gained `WritableRoot`, and a writable-root sandbox gets **no** implicit tmpfs over `/tmp`, `/root`, `/run` — with one, what a `RUN` step writes there would vanish in the next step while docker keeps it in the image. Refused loudly rather than ignored: `EXPOSE`, `HEALTHCHECK`, `STOPSIGNAL`, `ONBUILD`, `MAINTAINER` at parse time; `ARG`, `SHELL`, `ADD` and multi-stage named as stages 2 and 3; BuildKit's `RUN --mount` with the CapEff measurement as the reason; and the dozen `docker build` flags werkdock cannot keep the promise of. ## Verified - `go test ./...`, `go vet ./...`, `gofmt -l .` — green and empty. - A real sandbox integration test builds `FROM`/`WORKDIR`/`COPY`/`RUN`/`CMD` and then *runs* the result; it skips without bwrap/userns like the existing ones. - The apt path, which the levers exist for, is a second integration test pointed at a Debian base (`WERKDOCK_TEST_BASE_ROOTFS`). Run here: `nginx-common` configures, `dpkg-query` in the built image reports `install ok installed`, the apt fragment is gone from the result. - End to end through the CLI: `import` a 143 MB `debian:trixie` base with fakeroot, `build -t probe:1` with `apt-get install -y nginx jq` in the Dockerfile — **6.8 s** — then `run --rm probe:1` prints what its `RUN` steps and its `COPY` produced. Follow-ups the RFC names without deciding: store dedup between image versions, keeping layer blobs as a transport and cache format (the one with the highest running cost today), `doctor IMAGE`, and `ADD` from a URL. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Collaborator
⚠️ werkrevisor ERROR for 646179c, superseded by the result for 405f02b

⚠️ werkrevisor: ERROR / review not completed, human action needed

Change kind: unknown.

Reasons

  • ERROR the PR never changed from a 'WIP: ' title to one without; set 'WIP: ', push the next commit and then remove 'WIP: '
Usage: model `openrouter/minimax/minimax-m3`, LLM skipped (never marked ready), duration 0.1 s, no model call, cost 0 USD, config: defaults + host.
Reviewed head `646179c2a7f2a215c48f7923505e3e0b550a8d15` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.0.
<!-- werkrevisor-error:646179c2a7f2a215c48f7923505e3e0b550a8d15 --> <!-- werkrevisor-folded --> <details><summary>⚠️ werkrevisor ERROR for <code>646179c</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6586">the result for <code>405f02b</code></a></summary> ## ⚠️ werkrevisor: ERROR / review not completed, human action needed Change kind: `unknown`. ### Reasons - **ERROR** the PR never changed from a 'WIP: ' title to one without; set 'WIP: ', push the next commit and then remove 'WIP: ' <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM skipped (never marked ready), duration 0.1 s, no model call, cost 0 USD, config: defaults + host.<br> Reviewed head `646179c2a7f2a215c48f7923505e3e0b550a8d15` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.0. </sub> </details>
mi marked the pull request as work in progress 2026-10-20 14:17:08 +02:00
mi marked the pull request as ready for review 2026-10-20 14:17:22 +02:00
mi marked the pull request as work in progress 2026-10-20 14:23:09 +02:00
mi marked the pull request as ready for review 2026-10-20 14:31:03 +02:00
Collaborator
🟠 werkrevisor ORANGE for 646179c, superseded by the result for 405f02b

🟠 werkrevisor: ORANGE / FINDINGS

Change kind: feature_with_tests.

Reasons

  • ORANGE 1 finding(s) that Jev rates as probably real and severe: internal/build/build.go:314-323
  • YELLOW PR too large for an AI-only approval: 2319 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • hint writing rules violated in the changed docs: CHANGELOG.md:15: more than one sentence on a line; CHANGELOG.md:27: more than one sentence on a line; CHANGELOG.md:35: more than one sentence on a line; and 96 more
  • hint 18 changed file(s) that neither weaken the checks nor touch security according to Jev
  • hint 1 finding(s) of low severity or rated minor by Jev: internal/doctor/doctor.go:133-136
  • hint model recommends merge with caution

Findings

  1. Apt fragment removal fails after RUN step deletes it at internal/build/build.go:314-323, severity unknown, counted as medium, ORANGE, as a line comment

  2. Backend name hardcoded in doctor at internal/doctor/doctor.go:133-136, severity unknown, counted as medium, hint, as a line comment

Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 102441, calls 1, tokens in 98445 / out 20991 (reasoning included in out), duration 185.1 s, cost 0.0547 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 2, tokens in 2944, cost 0.0001 USD, build file judge `jev-1.13.0` calls 18, tokens in 33511, cost 0.0014 USD, phases: path_judge 1.2 s, context 0.6 s, model_review 182.4 s, finding_judge 0.3 s, config: defaults + host.
Reviewed head `646179c2a7f2a215c48f7923505e3e0b550a8d15` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.1.
<!-- werkrevisor:646179c2a7f2a215c48f7923505e3e0b550a8d15 --> <!-- werkrevisor-verdict:ORANGE --> <!-- werkrevisor-blocks:086ac51850ee49834b13a65aa1730fbc,0e8074063f1846bf4910926c267ab1b6,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,f76adf5526fe994c9c329c5119fa63cc,6028d389a45132790d20266e6cd74b5c,a99e953b9b4009a3e59c07fc8623cbd3,c506f00d7531b8c1f8159625d1af0d5c,accc3ec0a3d50e927d717f2a8682f05b,da5f319db91a51e49a6b21c88f17840f,0c57c2c46b90b19d0352d3b45767c0e7,92ab3f79eada83c62df245d41fbe08f8,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,ea35b8cf7f34a671a88f83f45eff9f6d,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,f674363556d1035095d5dd08e97b8263,79aa61b8ab9ebe6c5d1477fbcba0df4e,6a67037e7f17921fb282942511a6f1cf,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> <!-- werkrevisor-folded --> <details><summary>🟠 werkrevisor ORANGE for <code>646179c</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6586">the result for <code>405f02b</code></a></summary> ## 🟠 werkrevisor: ORANGE / FINDINGS Change kind: `feature_with_tests`. ### Reasons - **ORANGE** 1 finding(s) that Jev rates as probably real and severe: internal/build/build.go:314-323 - **YELLOW** PR too large for an AI-only approval: 2319 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **hint** writing rules violated in the changed docs: CHANGELOG.md:15: more than one sentence on a line; CHANGELOG.md:27: more than one sentence on a line; CHANGELOG.md:35: more than one sentence on a line; and 96 more - **hint** 18 changed file(s) that neither weaken the checks nor touch security according to Jev - **hint** 1 finding(s) of low severity or rated minor by Jev: internal/doctor/doctor.go:133-136 - **hint** model recommends merge with caution ### Findings 1. **Apt fragment removal fails after RUN step deletes it** at `internal/build/build.go:314-323`, severity unknown, counted as medium, ORANGE, as a line comment 2. **Backend name hardcoded in doctor** at `internal/doctor/doctor.go:133-136`, severity unknown, counted as medium, hint, as a line comment <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 102441, calls 1, tokens in 98445 / out 20991 (reasoning included in out), duration 185.1 s, cost 0.0547 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 2, tokens in 2944, cost 0.0001 USD, build file judge `jev-1.13.0` calls 18, tokens in 33511, cost 0.0014 USD, phases: path_judge 1.2 s, context 0.6 s, model_review 182.4 s, finding_judge 0.3 s, config: defaults + host.<br> Reviewed head `646179c2a7f2a215c48f7923505e3e0b550a8d15` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.1. </sub> </details>
ai-reviewer left a comment
Collaborator

werkrevisor: ORANGE / FINDINGS for 646179c2a7f2a215c48f7923505e3e0b550a8d15.
The result comment of werkrevisor holds the details.

werkrevisor: ORANGE / FINDINGS for `646179c2a7f2a215c48f7923505e3e0b550a8d15`. The result comment of werkrevisor holds the details. <!-- werkrevisor-review:646179c2a7f2a215c48f7923505e3e0b550a8d15 -->
CHANGELOG.md Outdated
@@ -12,0 +12,4 @@
## 0.12.0 (2026-10-17)
Theme: the verb that was "not planned". `werkdock build` builds an image from
a Dockerfile on the target itself — no second machine, no upload. RFC 0004
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +24,4 @@
`FROM` resolves through the store and pulls on a miss, one writable work
tree is created over the base, the steps run against it, and the result is
an ordinary flat werkdock image — `images`, `inspect`, `rmi`, `run` and
`--mount type=image` apply to it unchanged. Staging is the mechanism
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +32,4 @@
metadata instructions `import -c` already knew — `ENTRYPOINT`, `CMD`,
`ENV`, `WORKDIR`, `USER`, `VOLUME`, `LABEL` — through the same
`store.ApplyChange`, so a Dockerfile line and a `--change` line are parsed
by one piece of code. Line continuations with `\`, comments and blank
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +35,4 @@
by one piece of code. Line continuations with `\`, comments and blank
lines are handled; `RUN` takes the shell and the exec form.
- **Both uid levers are werkdock's business, not the Dockerfile's**, so the
same Dockerfile still builds under docker. Measured on a Debian trixie
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +45,4 @@
calls the real syscall and reports its `EINVAL`. `fakeroot` plus
`FAKEROOTDONTTRYCHOWN=1` does, so `RUN` steps are wrapped in `fakeroot`
when the base image has one, and `build` says so up front when it has not.
- **Two backends behind one interface, of which one is implemented.** The
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +47,4 @@
when the base image has one, and `build` says so up front when it has not.
- **Two backends behind one interface, of which one is implemented.** The
copy backend (a real full copy of the base, root bound read-write) works on
the bwrap 0.8 the webspaces run today. The overlay backend is designed, not
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +50,4 @@
the bwrap 0.8 the webspaces run today. The overlay backend is designed, not
written: `--overlay-src` stacks unprivileged on bwrap 0.11 and its upper
directory is already a layer diff — measured, including deletions appearing
as character devices 0:0, which is what OCI writes as `.wh.NAME`. It is
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +52,4 @@
directory is already a layer diff — measured, including deletions appearing
as character devices 0:0, which is what OCI writes as `.wh.NAME`. It is
what the per-step cache waits for; without a union mount a cached step
would cost a whole tree copy. The copy backend stays as the fallback.
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +59,4 @@
### Changed
- **`engine.RunSpec` has `WritableRoot`**: the rootfs is bound with `--bind`
instead of `--ro-bind`. Only build steps set it. A sandbox with a writable
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
CHANGELOG.md Outdated
@@ -12,0 +62,4 @@
instead of `--ro-bind`. Only build steps set it. A sandbox with a writable
root also gets **no implicit tmpfs** over `/tmp`, `/root` and `/run` — it
does not need one, and with one, what a `RUN` step writes there would be
gone in the next step while docker keeps it in the image. Runs are
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.

Fixed in `d4cb387`: the section 0.12.0 has one sentence per line, its wording unchanged.
claude-code marked this conversation as resolved
@@ -0,0 +311,4 @@
return nil
}
func (b *builder) removeAptFragment(rootfs string) error {
Collaborator

Apt fragment removal fails after RUN step deletes it, severity unknown, counted as medium (werkrevisor)

ORANGE; Jev: real 0.87, severity 2.9 of 3

removeAptFragment calls os.Remove(target) unconditionally when the build wrote the fragment. If a later RUN step — or apt-get itself, or a postinst that cleans /etc/apt/apt.conf.d/ — deletes the file, the build fails at commit with the cryptic remove /etc/apt/apt.conf.d/00werkdock-sandbox: no such file or directory. The user's only action was something normal; the error names a file the user did not write and does not know about. The fix is one line: ignore os.IsNotExist(err) from os.Remove (or re-stat the file and skip the remove when absent).

**Apt fragment removal fails after RUN step deletes it**, severity unknown, counted as medium (werkrevisor) ORANGE; Jev: real 0.87, severity 2.9 of 3 `removeAptFragment` calls `os.Remove(target)` unconditionally when the build wrote the fragment. If a later `RUN` step — or `apt-get` itself, or a postinst that cleans `/etc/apt/apt.conf.d/` — deletes the file, the build fails at commit with the cryptic `remove /etc/apt/apt.conf.d/00werkdock-sandbox: no such file or directory`. The user's only action was something normal; the error names a file the user did not write and does not know about. The fix is one line: ignore `os.IsNotExist(err)` from `os.Remove` (or re-stat the file and skip the remove when absent).
Collaborator

Fixed in 405f02b: removeAptFragment ignores os.IsNotExist; TestRunToleratesAStepThatRemovedTheAptFragment fails without the fix and passes with it.

Fixed in `405f02b`: `removeAptFragment` ignores `os.IsNotExist`; `TestRunToleratesAStepThatRemovedTheAptFragment` fails without the fix and passes with it.
claude-code marked this conversation as resolved
@@ -99,0 +130,4 @@
r.pass("a writable root bind works (werkdock build uses the %s backend)", BuildBackend)
}
// BuildBackend is the name of the build backend this host gets. It is
Collaborator

Backend name hardcoded in doctor, severity unknown, counted as medium (werkrevisor)

hint; Jev: real 0.83, severity 2.2 of 3

BuildBackend = "copy" is a constant used in the doctor's pass message, while build.Select() (called from the CLI) is the source of truth. The CHANGELOG and the constant's own comment promise they "never disagree", but they are not connected: when the overlay backend is added and Select() starts returning "overlay", doctor will still print copy backend until the constant is updated by hand. Doctor should call build.Select().Name() (or be moved behind the build package) so the contract holds automatically. Today it is silent because both happen to say "copy"; it is fragile for a stated invariant.

**Backend name hardcoded in doctor**, severity unknown, counted as medium (werkrevisor) hint; Jev: real 0.83, severity 2.2 of 3 `BuildBackend = "copy"` is a constant used in the doctor's pass message, while `build.Select()` (called from the CLI) is the source of truth. The CHANGELOG and the constant's own comment promise they "never disagree", but they are not connected: when the overlay backend is added and `Select()` starts returning `"overlay"`, doctor will still print `copy backend` until the constant is updated by hand. Doctor should call `build.Select().Name()` (or be moved behind the build package) so the contract holds automatically. Today it is silent because both happen to say "copy"; it is fragile for a stated invariant.
Collaborator

Fixed in 405f02b: doctor reads the name from build.Select(); the constant BuildBackend is gone.

Fixed in `405f02b`: doctor reads the name from `build.Select()`; the constant `BuildBackend` is gone.
claude-code marked this conversation as resolved
ai-reviewer scheduled this pull request to auto merge when all checks succeed 2026-10-20 14:34:49 +02:00
Collaborator
🟡 werkrevisor YELLOW for 405f02b, superseded by the result for 26225c2

🟡 werkrevisor: YELLOW / HUMAN_REVIEW

Change kind: feature_with_tests.

Reasons

  • YELLOW PR too large for an AI-only approval: 2333 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • hint writing rules violated in the changed docs: CHANGELOG.md:15: more than one sentence on a line; CHANGELOG.md:27: more than one sentence on a line; CHANGELOG.md:35: more than one sentence on a line; and 96 more
  • hint 18 changed file(s) that neither weaken the checks nor touch security according to Jev
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 102945, calls 1, tokens in 98918 / out 17311 (reasoning included in out), duration 214.8 s, cost 0.0504 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, build file judge `jev-1.13.0` calls 18, tokens in 33691, cost 0.0014 USD, phases: path_judge 1.1 s, context 0.6 s, model_review 212.2 s, config: defaults + host.
Reviewed head `405f02b335bce02120e2f7405d448d48c2183faa` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.1.
<!-- werkrevisor:405f02b335bce02120e2f7405d448d48c2183faa --> <!-- werkrevisor-verdict:YELLOW --> <!-- werkrevisor-blocks:086ac51850ee49834b13a65aa1730fbc,0e8074063f1846bf4910926c267ab1b6,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,f76adf5526fe994c9c329c5119fa63cc,6028d389a45132790d20266e6cd74b5c,730b1f2341308d2279f8fdac245d02f5,5f85265825bba644b6a06f20d3411e57,accc3ec0a3d50e927d717f2a8682f05b,da5f319db91a51e49a6b21c88f17840f,0c57c2c46b90b19d0352d3b45767c0e7,92ab3f79eada83c62df245d41fbe08f8,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,ea35b8cf7f34a671a88f83f45eff9f6d,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,900df901cad2eb32989f0e21fc6894d9,ae8daa06a6a9b1a224b665d0f9bc0953,6a67037e7f17921fb282942511a6f1cf,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> <!-- werkrevisor-folded --> <details><summary>🟡 werkrevisor YELLOW for <code>405f02b</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6606">the result for <code>26225c2</code></a></summary> ## 🟡 werkrevisor: YELLOW / HUMAN_REVIEW Change kind: `feature_with_tests`. ### Reasons - **YELLOW** PR too large for an AI-only approval: 2333 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **hint** writing rules violated in the changed docs: CHANGELOG.md:15: more than one sentence on a line; CHANGELOG.md:27: more than one sentence on a line; CHANGELOG.md:35: more than one sentence on a line; and 96 more - **hint** 18 changed file(s) that neither weaken the checks nor touch security according to Jev <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 102945, calls 1, tokens in 98918 / out 17311 (reasoning included in out), duration 214.8 s, cost 0.0504 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, build file judge `jev-1.13.0` calls 18, tokens in 33691, cost 0.0014 USD, phases: path_judge 1.1 s, context 0.6 s, model_review 212.2 s, config: defaults + host.<br> Reviewed head `405f02b335bce02120e2f7405d448d48c2183faa` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.1. </sub> </details>
mi added 3 commits 2026-10-20 15:03:34 +02:00
RFC 0004 had ruled `build` out because it would need "writable rootfs
snapshots per step and `pull` for FROM". `pull` arrived in 0.3.0, and
per-step snapshots turned out to be what a build *cache* needs, not what
a build needs: one writable tree over the base is enough.

RFC 0006 decides the build model, cuts the one version-dependent
interface, and records what was measured on a Debian trixie rootfs with
bwrap 0.11.1 and a single-uid map: a writable root bind works
unprivileged, CapEff is 0 so the sandbox can mount nothing itself, apt
needs APT::Sandbox::User=root, fakeroot alone does not fix dpkg's chown
but fakeroot plus FAKEROOTDONTTRYCHOWN=1 does, and overlay stacks with
its upper directory already being a layer diff. It also separates the
three things "no layers" has been made to mean, so the README's formula
stops carrying more than it supports.

Implemented: stage 1 on the copy backend. `werkdock build [-f
Dockerfile] -t IMAGE CONTEXT` with FROM, RUN, COPY and the metadata
instructions ApplyChange already knew, committing through the staging
and locking mechanics import has used since 0.11.0. The overlay backend
is deliberately not implemented — only the interface is cut so it fits.

Measured end to end: a 143 MB debian:trixie base with fakeroot, a
Dockerfile with `apt-get install -y nginx jq` in it, 6.8 s for the whole
build; nginx-common configures, dpkg-query in the result reports
"install ok installed", and `werkdock run --rm` executes its CMD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findings of werkrevisor on PR#14: removeAptFragment ignores a fragment that is gone,
and doctor reads the name from build.Select() instead of a constant of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG 0.12.0: one sentence per line, wording unchanged
werkator build succeeded after 00:03
werkator/dependencies build succeeded after 00:07
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; review request failed: POST /repos/mi/werkdock/pulls/14/requested_reviewers failed with HTTP 422
ai-review/gate build failed after 03:58
26225c245c
The writing rule of werkrevisor on PR#14; the older sections keep their layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mi force-pushed feature/dockerfile-builds from d4cb387abd to 26225c245c 2026-10-20 15:03:34 +02:00 Compare
Collaborator
🟠 werkrevisor ORANGE for 26225c2, superseded by the result for 9e4ba14

🟠 werkrevisor: ORANGE / FINDINGS

Change kind: feature_with_tests.

Reasons

  • ORANGE 1 finding(s) that Jev rates as probably real and severe: internal/doctor/doctor.go:108-135
  • YELLOW PR too large for an AI-only approval: 2333 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • YELLOW Sandbox isolation according to Jev: internal/build/build.go (0.87), internal/engine/bwrap.go (0.94); a human checks it, unless you revert the change because the task does not need it
  • hint writing rules violated in the changed docs: README.md:10: more than one sentence on a line; README.md:10: line longer than 128 characters (2249); README.md:20: line longer than 128 characters (141); and 142 more
  • hint 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
  • hint 1 finding(s) possibly severe, but Jev doubts that they are real: internal/build/build.go:254-259 (advisory: Jev judged every changed file)
  • hint 1 finding(s) of low severity or rated minor by Jev: internal/build/build.go:238-247
  • hint model recommends merge with caution

Findings

  1. Build probe does not exercise root writability at internal/doctor/doctor.go:108-135, severity unknown, counted as medium, ORANGE, as a line comment

  2. resolvConf bind assumes base has the file at internal/build/build.go:254-259, severity unknown, counted as medium, YELLOW, as a line comment

  3. runCommand reuses CMD parsing at internal/build/build.go:238-247, severity unknown, counted as medium, hint, as a line comment

Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 104022, calls 1, tokens in 99951 / out 31203 (reasoning included in out), duration 235.3 s, cost 0.0674 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 3, tokens in 6455, cost 0.0003 USD, build file judge `jev-1.13.0` calls 18, tokens in 39243, cost 0.0016 USD, phases: path_judge 1.2 s, context 0.6 s, model_review 232.7 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).
Reviewed head `26225c245cfc5e09e4a685c3e4049b6d8d5a6240` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
<!-- werkrevisor:26225c245cfc5e09e4a685c3e4049b6d8d5a6240 --> <!-- werkrevisor-verdict:ORANGE --> <!-- werkrevisor-blocks:4f41e79771d454bd5f2213aee8a987a5,0e8074063f1846bf4910926c267ab1b6,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,f76adf5526fe994c9c329c5119fa63cc,6028d389a45132790d20266e6cd74b5c,730b1f2341308d2279f8fdac245d02f5,5f85265825bba644b6a06f20d3411e57,accc3ec0a3d50e927d717f2a8682f05b,da5f319db91a51e49a6b21c88f17840f,0c57c2c46b90b19d0352d3b45767c0e7,92ab3f79eada83c62df245d41fbe08f8,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,135caa91eb6b88e57e28cba7c29e3e37,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,900df901cad2eb32989f0e21fc6894d9,ae8daa06a6a9b1a224b665d0f9bc0953,6a67037e7f17921fb282942511a6f1cf,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> <!-- werkrevisor-folded --> <details><summary>🟠 werkrevisor ORANGE for <code>26225c2</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6628">the result for <code>9e4ba14</code></a></summary> ## 🟠 werkrevisor: ORANGE / FINDINGS Change kind: `feature_with_tests`. ### Reasons - **ORANGE** 1 finding(s) that Jev rates as probably real and severe: internal/doctor/doctor.go:108-135 - **YELLOW** PR too large for an AI-only approval: 2333 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **YELLOW** Sandbox isolation according to Jev: internal/build/build.go (0.87), internal/engine/bwrap.go (0.94); a human checks it, unless you revert the change because the task does not need it - **hint** writing rules violated in the changed docs: README.md:10: more than one sentence on a line; README.md:10: line longer than 128 characters (2249); README.md:20: line longer than 128 characters (141); and 142 more - **hint** 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev - **hint** 1 finding(s) possibly severe, but Jev doubts that they are real: internal/build/build.go:254-259 (advisory: Jev judged every changed file) - **hint** 1 finding(s) of low severity or rated minor by Jev: internal/build/build.go:238-247 - **hint** model recommends merge with caution ### Findings 1. **Build probe does not exercise root writability** at `internal/doctor/doctor.go:108-135`, severity unknown, counted as medium, ORANGE, as a line comment 2. **resolvConf bind assumes base has the file** at `internal/build/build.go:254-259`, severity unknown, counted as medium, YELLOW, as a line comment 3. **runCommand reuses CMD parsing** at `internal/build/build.go:238-247`, severity unknown, counted as medium, hint, as a line comment <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 104022, calls 1, tokens in 99951 / out 31203 (reasoning included in out), duration 235.3 s, cost 0.0674 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 3, tokens in 6455, cost 0.0003 USD, build file judge `jev-1.13.0` calls 18, tokens in 39243, cost 0.0016 USD, phases: path_judge 1.2 s, context 0.6 s, model_review 232.7 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).<br> Reviewed head `26225c245cfc5e09e4a685c3e4049b6d8d5a6240` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1. </sub> </details>
ai-reviewer left a comment
Collaborator

werkrevisor: ORANGE / FINDINGS for 26225c245cfc5e09e4a685c3e4049b6d8d5a6240.
The result comment of werkrevisor holds the details.

werkrevisor: ORANGE / FINDINGS for `26225c245cfc5e09e4a685c3e4049b6d8d5a6240`. The result comment of werkrevisor holds the details. <!-- werkrevisor-review:26225c245cfc5e09e4a685c3e4049b6d8d5a6240 -->
@@ -0,0 +235,4 @@
// runCommand is the RUN instruction's command: a JSON array is the
// exec form and runs as given, anything else is the shell form, which
// docker runs through /bin/sh -c (SHELL is stage 2).
func runCommand(args string) ([]string, error) {
Collaborator

runCommand reuses CMD parsing, severity unknown, counted as medium (werkrevisor)

hint; Jev: real 0.84, severity 1.5 of 3

runCommand parses RUN's exec form by feeding the args through store.ApplyChange(&config, "CMD "+args). When the JSON is malformed the returned error references CMD, not RUN, so a RUN ["bad" yields a confusing "CMD ..." message even though the user wrote RUN. The function is only used for RUN.

**runCommand reuses CMD parsing**, severity unknown, counted as medium (werkrevisor) hint; Jev: real 0.84, severity 1.5 of 3 `runCommand` parses RUN's exec form by feeding the args through `store.ApplyChange(&config, "CMD "+args)`. When the JSON is malformed the returned error references CMD, not RUN, so a `RUN ["bad"` yields a confusing "CMD ..." message even though the user wrote RUN. The function is only used for RUN.
Collaborator

Fixed in 9e4ba14: the error starts with RUN <args>: and no longer names CMD. Test: TestRunCommandNamesRunInAParseError.

Fixed in `9e4ba14`: the error starts with `RUN <args>:` and no longer names CMD. Test: `TestRunCommandNamesRunInAParseError`.
claude-code marked this conversation as resolved
@@ -0,0 +251,4 @@
// file exists in practically every base image, so the bind leaves no
// mountpoint behind in the finished tree — which it would, since bwrap
// creates a missing one in the writable root and it stays (measured).
func resolvConfMount() []engine.Mount {
Collaborator

resolvConf bind assumes base has the file, severity unknown, counted as medium (werkrevisor)

YELLOW; Jev: real 0.48, severity 2.8 of 3

resolvConfMount checks only the host's /etc/resolv.conf, not the base image's. When the base lacks it (e.g. FROM scratch), EnsureMountpoints pre-creates an empty file at /etc/resolv.conf in the work tree; the build then commits it into the finished image, silently breaking DNS in the produced image. The RFC acknowledges this in prose but the code does not guard against it.

**resolvConf bind assumes base has the file**, severity unknown, counted as medium (werkrevisor) YELLOW; Jev: real 0.48, severity 2.8 of 3 `resolvConfMount` checks only the host's `/etc/resolv.conf`, not the base image's. When the base lacks it (e.g. `FROM scratch`), `EnsureMountpoints` pre-creates an empty file at `/etc/resolv.conf` in the work tree; the build then commits it into the finished image, silently breaking DNS in the produced image. The RFC acknowledges this in prose but the code does not guard against it.
Collaborator

Fixed in 9e4ba14: a RUN step binds the host's resolv.conf only where the tree has a regular one, so no empty file stays; RFC 0006 says so. Test: TestRunStepBindsTheResolverOnlyWhereTheBaseHasOne.

Fixed in `9e4ba14`: a RUN step binds the host's resolv.conf only where the tree has a regular one, so no empty file stays; RFC 0006 says so. Test: `TestRunStepBindsTheResolverOnlyWhereTheBaseHasOne`.
claude-code marked this conversation as resolved
@@ -97,0 +105,4 @@
// the host is touched. The backend named here is what build will pick;
// an overlay backend, once bwrap on the target is new enough and not
// installed setuid, is reported in its place.
func buildChecks(r *Report, run Runner) {
Collaborator

Build probe does not exercise root writability, severity unknown, counted as medium (werkrevisor)

ORANGE; Jev: real 0.86, severity 2.7 of 3

The build probe writes to /tmp while the sandbox already has --tmpfs /tmp, so the touch succeeds regardless of whether the root bind is actually writable. The probe therefore only confirms bwrap accepted --bind / /; a host where the root is mounted read-only at a non-tmpfs path would still pass the check and only fail later inside a real RUN step. Worth touching a path the tmpfs does not cover.

**Build probe does not exercise root writability**, severity unknown, counted as medium (werkrevisor) ORANGE; Jev: real 0.86, severity 2.7 of 3 The build probe writes to `/tmp` while the sandbox already has `--tmpfs /tmp`, so the touch succeeds regardless of whether the root bind is actually writable. The probe therefore only confirms bwrap accepted `--bind / /`; a host where the root is mounted read-only at a non-tmpfs path would still pass the check and only fail later inside a real RUN step. Worth touching a path the tmpfs does not cover.
Collaborator

Fixed in 9e4ba14: the probe touches a file in a directory of its own below the target directory, through the root bind, without a tmpfs, and removes it; with --ro-bind it fails ("Read-only file system"), checked by hand. Tests: TestBuildProbeWritesThroughTheRootBindBelowTheTarget, TestBuildProbeFailsWithoutAPlaceBelowTheTarget.

Fixed in `9e4ba14`: the probe touches a file in a directory of its own below the target directory, through the root bind, without a tmpfs, and removes it; with `--ro-bind` it fails ("Read-only file system"), checked by hand. Tests: `TestBuildProbeWritesThroughTheRootBindBelowTheTarget`, `TestBuildProbeFailsWithoutAPlaceBelowTheTarget`.
claude-code marked this conversation as resolved
mi added 1 commit 2026-10-20 15:11:29 +02:00
doctor's build probe writes through the root bind; resolv.conf only where the tree has it; RUN names itself in a parse error
werkator build succeeded after 00:03
werkator/dependencies build succeeded after 00:07
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; review request failed: POST /repos/mi/werkdock/pulls/14/requested_reviewers failed with HTTP 422
ai-review/gate build failed after 02:31
9e4ba14371
Findings of werkrevisor on PR#14: the probe wrote into a tmpfs, which takes the write whatever the bind is;
a bind of a missing resolv.conf left an empty one in the image; a malformed exec form named CMD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
🟠 werkrevisor ORANGE for 9e4ba14, superseded by the result for ce98a65

🟠 werkrevisor: ORANGE / FINDINGS

Change kind: feature_with_tests.

Reasons

  • ORANGE 1 finding(s) that Jev rates as probably real and severe: README.md:10
  • YELLOW PR too large for an AI-only approval: 2434 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • YELLOW Sandbox isolation according to Jev: internal/build/build.go (0.85), internal/engine/bwrap.go (0.93); a human checks it, unless you revert the change because the task does not need it
  • hint writing rules violated in the changed docs: README.md:10: more than one sentence on a line; README.md:10: line longer than 128 characters (2249); README.md:20: line longer than 128 characters (141); and 142 more
  • hint 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
  • hint model recommends merge with caution

Findings

  1. Doc/code contradiction at README.md:10, severity unknown, counted as medium, ORANGE, as a line comment
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 107310, calls 1, tokens in 103128 / out 11483 (reasoning included in out), duration 147.8 s, cost 0.0447 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 1, tokens in 3610, cost 0.0002 USD, build file judge `jev-1.13.0` calls 18, tokens in 40616, cost 0.0017 USD, phases: path_judge 1.2 s, context 0.6 s, model_review 145.1 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).
Reviewed head `9e4ba14371625257da3d33407777eb9e51baa0af` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
<!-- werkrevisor:9e4ba14371625257da3d33407777eb9e51baa0af --> <!-- werkrevisor-verdict:ORANGE --> <!-- werkrevisor-blocks:4f41e79771d454bd5f2213aee8a987a5,0e8074063f1846bf4910926c267ab1b6,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,d74a79a10b6bfc94455190e03d986d3d,6028d389a45132790d20266e6cd74b5c,d9ee6fec8fa0330b22c52df22d132de9,37a001f5d306b37459a642eaac630a99,accc3ec0a3d50e927d717f2a8682f05b,da5f319db91a51e49a6b21c88f17840f,0c57c2c46b90b19d0352d3b45767c0e7,92ab3f79eada83c62df245d41fbe08f8,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,135caa91eb6b88e57e28cba7c29e3e37,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,900df901cad2eb32989f0e21fc6894d9,c3cc136c9ff5fbe73b2d2f5a3ce33658,ff866b05d11084c5b93697ce6d4cdf7f,5efcc24145ba83a73d84f8b4dbcb72b1,b61c58c22bd2669dc41f2922313c29d4,6301a395b54ced74a4baaf84c01d2e80,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> <!-- werkrevisor-folded --> <details><summary>🟠 werkrevisor ORANGE for <code>9e4ba14</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6640">the result for <code>ce98a65</code></a></summary> ## 🟠 werkrevisor: ORANGE / FINDINGS Change kind: `feature_with_tests`. ### Reasons - **ORANGE** 1 finding(s) that Jev rates as probably real and severe: README.md:10 - **YELLOW** PR too large for an AI-only approval: 2434 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **YELLOW** Sandbox isolation according to Jev: internal/build/build.go (0.85), internal/engine/bwrap.go (0.93); a human checks it, unless you revert the change because the task does not need it - **hint** writing rules violated in the changed docs: README.md:10: more than one sentence on a line; README.md:10: line longer than 128 characters (2249); README.md:20: line longer than 128 characters (141); and 142 more - **hint** 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev - **hint** model recommends merge with caution ### Findings 1. **Doc/code contradiction** at `README.md:10`, severity unknown, counted as medium, ORANGE, as a line comment <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 107310, calls 1, tokens in 103128 / out 11483 (reasoning included in out), duration 147.8 s, cost 0.0447 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 1, tokens in 3610, cost 0.0002 USD, build file judge `jev-1.13.0` calls 18, tokens in 40616, cost 0.0017 USD, phases: path_judge 1.2 s, context 0.6 s, model_review 145.1 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).<br> Reviewed head `9e4ba14371625257da3d33407777eb9e51baa0af` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1. </sub> </details>
ai-reviewer left a comment
Collaborator

werkrevisor: ORANGE / FINDINGS for 9e4ba14371625257da3d33407777eb9e51baa0af.
The result comment of werkrevisor holds the details.

werkrevisor: ORANGE / FINDINGS for `9e4ba14371625257da3d33407777eb9e51baa0af`. The result comment of werkrevisor holds the details. <!-- werkrevisor-review:9e4ba14371625257da3d33407777eb9e51baa0af -->
README.md Outdated
@@ -8,3 +8,3 @@
- An *image* is a rootfs archive; an *instance* is an unpacked, writable directory tree and corresponds to a docker container.
- `import` takes a plain rootfs archive, as `docker import` does; `load` takes a `docker save` archive or an OCI image layout, flattens the layers (whiteouts honored, paths resolved inside the rootfs as in a chroot) and keeps the OCI config — so a Dockerfile-built image lands with its ENTRYPOINT, CMD, ENV, and WORKDIR ([RFC 0004](docs/rfcs/0004-docker-compatible-image-creation.md)). `load -i … --name` still routes a rootfs archive to `import`, for callers that predate it. `pull` fetches an image from a registry the same way, with the registry side (auth, manifest lists, blobs) delegated to the [crane](https://github.com/google/go-containerregistry) binary — a single unprivileged executable found in `PATH`, beside `werkdock`, or at `WERKDOCK_CRANE`; it reads `~/.docker/config.json` (`DOCKER_CONFIG`) for credentials as docker does. `images` lists one reference per line by default (what a script matches against) and takes docker's `--format`, with `--format table` giving docker's columns; `rmi IMAGE...` removes images.
- `import` takes a plain rootfs archive, as `docker import` does; `load` takes a `docker save` archive or an OCI image layout, flattens the layers (whiteouts honored, paths resolved inside the rootfs as in a chroot) and keeps the OCI config — so a Dockerfile-built image lands with its ENTRYPOINT, CMD, ENV, and WORKDIR ([RFC 0004](docs/rfcs/0004-docker-compatible-image-creation.md)). `load -i … --name` still routes a rootfs archive to `import`, for callers that predate it. `pull` fetches an image from a registry the same way, with the registry side (auth, manifest lists, blobs) delegated to the [crane](https://github.com/google/go-containerregistry) binary — a single unprivileged executable found in `PATH`, beside `werkdock`, or at `WERKDOCK_CRANE`; it reads `~/.docker/config.json` (`DOCKER_CONFIG`) for credentials as docker does. `images` lists one reference per line by default (what a script matches against) and takes docker's `--format`, with `--format table` giving docker's columns; `rmi IMAGE...` removes images.- `build` builds an image from a Dockerfile on this host — `werkdock build [-f Dockerfile] -t IMAGE CONTEXT`, docker's flags ([RFC 0006](docs/rfcs/0006-dockerfile-builds.md)). `FROM` resolves through the store and pulls on a miss, one writable work tree is created over the base, the steps run against it, and the result is an ordinary flat image. Stage 1 is `FROM`, `RUN`, `COPY` plus the metadata instructions `import -c` already knew; `ARG`, `SHELL`, `ADD` and multi-stage builds name the stage of RFC 0006 that brings them, and `EXPOSE`, `HEALTHCHECK`, `STOPSIGNAL`, `ONBUILD` and BuildKit's `RUN --mount` are refused with the reason at parse time. Werkdock sets the two levers an unprivileged single-uid sandbox needs itself, so the Dockerfile stays portable: `APT::Sandbox::User root` for the duration of the build, and `fakeroot` with `FAKEROOTDONTTRYCHOWN=1` around every `RUN` step — which means **the base image needs `fakeroot` installed**, or a package whose postinst chowns fails. There is **no build cache**: every build starts at `FROM`, because caching a step without a union mount would cost a whole tree copy. That changes with the overlay backend, after the webspaces move to Debian 13 (bwrap 0.11).
Collaborator

Doc/code contradiction, severity unknown, counted as medium (werkrevisor)

ORANGE; Jev: real 0.87, severity 2.8 of 3

The README, the "Build flags" block in CLI usage, and the CHANGELOG claim that "BuildKit's RUN --mount is refused with the reason at parse time" alongside EXPOSE/HEALTHCHECK/STOPSIGNAL/ONBUILD. The parser (internal/build/dockerfile.go, parseLine) only refuses the standalone instruction keywords EXPOSE, HEALTHCHECK, STOPSIGNAL, ONBUILD and MAINTAINER; a RUN --mount=... line is split into keyword="RUN" and args="--mount=... ..." and is executed as a shell command via runCommand, so the user gets a generic shell error (--mount=...: not found, or mknod/mount-EPERM) rather than a specific werkdock refusal before the 2 GiB tree copy starts. The CHANGELOG's own wording is internally consistent (it groups BuildKit's RUN --mount under the mknod/mount section, not under "fail at parse time"), but the README/help text read as if it were a parse-time refusal, as the RFC had said ("Refused loudly, not ignored.").

**Doc/code contradiction**, severity unknown, counted as medium (werkrevisor) ORANGE; Jev: real 0.87, severity 2.8 of 3 The README, the "Build flags" block in CLI usage, and the CHANGELOG claim that "BuildKit's RUN --mount is refused with the reason at parse time" alongside EXPOSE/HEALTHCHECK/STOPSIGNAL/ONBUILD. The parser (`internal/build/dockerfile.go`, `parseLine`) only refuses the standalone instruction keywords EXPOSE, HEALTHCHECK, STOPSIGNAL, ONBUILD and MAINTAINER; a `RUN --mount=...` line is split into keyword="RUN" and args="--mount=... ..." and is executed as a shell command via `runCommand`, so the user gets a generic shell error (`--mount=...: not found`, or mknod/mount-EPERM) rather than a specific werkdock refusal before the 2 GiB tree copy starts. The CHANGELOG's own wording is internally consistent (it groups BuildKit's RUN --mount under the mknod/mount section, not under "fail at parse time"), but the README/help text read as if it were a parse-time refusal, as the RFC had said ("Refused loudly, not ignored.").
Collaborator

Fixed in ce98a65: the parser refuses RUN --mount, --network and --security at parse time with the reason, as README, help and CHANGELOG say; cases in TestParseRefusesWithTheReason.

Fixed in `ce98a65`: the parser refuses `RUN --mount`, `--network` and `--security` at parse time with the reason, as README, help and CHANGELOG say; cases in `TestParseRefusesWithTheReason`.
claude-code marked this conversation as resolved
@@ -0,0 +83,4 @@
Two details the implementation forced, both worth writing down because they change behaviour:
- **A build step gets no implicit tmpfs.** Every `run` mounts a tmpfs over `/tmp`, `/root` and `/run`, because a read-only root has no writable place otherwise (RFC 0005). A build step's root *is* writable, so it needs none — and must not have them, or what a `RUN` step writes to `/tmp` or `/root` would be gone in the next step while docker keeps it in the image. `~/.npmrc` written in one step and read in the next is a common enough idiom that the alternative is a bug factory. So the implicit tmpfs mounts are tied to the read-only root.
- **A build binds as little as possible.** With a writable root bwrap creates a missing bind mountpoint itself, and the mountpoint stays in the tree (measured). The only bind a `RUN` step gets is the host's `/etc/resolv.conf`, read-only, as `run` does — and only where the tree already has the file, so nothing is left behind; a step over a base without one, e.g. `FROM scratch`, runs without the host's resolver. Anything else a build step might want mounted would have to earn a `build` flag first.
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Writing rule: line longer than 128 characters (500) (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: line longer than 128 characters (500)** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in ce98a65: one sentence per line.

Fixed in `ce98a65`: one sentence per line.
Collaborator

Fixed in ce98a65: every line of the paragraph is at most 128 characters.

Fixed in `ce98a65`: every line of the paragraph is at most 128 characters.
claude-code marked this conversation as resolved
mi added 1 commit 2026-10-20 15:16:48 +02:00
RUN --mount and BuildKit's other RUN flags are refused at parse time, as the docs say
werkator build succeeded after 00:03
werkator/dependencies build succeeded after 00:05
ai-review/assessment human review required; a human approval unlocks the merge; review request failed: POST /repos/mi/werkdock/pulls/14/requested_reviewers failed with HTTP 422
ai-review/gate build failed after 04:01
ce98a6560f
Finding of werkrevisor on PR#14; the RFC's paragraph on binds has one sentence per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
🟡 werkrevisor YELLOW for ce98a65, superseded by the result for 11af83c

🟡 werkrevisor: YELLOW / HUMAN_REVIEW

Change kind: feature_with_tests.

Reasons

  • YELLOW PR too large for an AI-only approval: 2450 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • YELLOW Sandbox isolation according to Jev: internal/build/build.go (0.86), internal/engine/bwrap.go (0.94); a human checks it, unless you revert the change because the task does not need it
  • hint writing rules violated in the changed docs: README.md:10: more than one sentence on a line; README.md:10: line longer than 128 characters (2249); README.md:20: line longer than 128 characters (141); and 140 more
  • hint 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
  • hint 1 finding(s) of low severity or rated minor by Jev: README.md:10
  • hint model recommends merge with caution

Findings

  1. README formatting bug at README.md:10, severity unknown, counted as medium

    hint; Jev: real 0.96, severity 1.6 of 3

    The new build bullet was appended directly to the end of the existing
    import/load/pull/images/rmi line without a newline separator, producing
    a single very long line that ends with "removes images.- build builds
    ... (bwrap 0.11).". In the rendered Markdown the build text is part of
    the import bullet, not its own list item, so the new build verb is
    effectively undocumented in the README. The CLI help text and the
    CHANGELOG entry document it, but the README's bullets under "Semantics"
    need a newline between "removes images." and "- build builds..." so it
    is a proper list item.

Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 107989, calls 1, tokens in 103785 / out 29344 (reasoning included in out), duration 238.1 s, cost 0.0651 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 1, tokens in 3518, cost 0.0001 USD, build file judge `jev-1.13.0` calls 18, tokens in 40919, cost 0.0017 USD, phases: path_judge 1.2 s, context 0.9 s, model_review 235.2 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).
Reviewed head `ce98a6560fc6f6afc423277241aeb79c920bdfb0` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
<!-- werkrevisor:ce98a6560fc6f6afc423277241aeb79c920bdfb0 --> <!-- werkrevisor-verdict:YELLOW --> <!-- werkrevisor-blocks:4f41e79771d454bd5f2213aee8a987a5,0e8074063f1846bf4910926c267ab1b6,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,b2625ae8384c82b801a82b6cbef97e5e,6028d389a45132790d20266e6cd74b5c,d9ee6fec8fa0330b22c52df22d132de9,37a001f5d306b37459a642eaac630a99,accc3ec0a3d50e927d717f2a8682f05b,da5f319db91a51e49a6b21c88f17840f,690fe8d5c8b477656f88804d7a1ac9bc,48ba638de328608a1c4bcdb7bddd7566,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,135caa91eb6b88e57e28cba7c29e3e37,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,900df901cad2eb32989f0e21fc6894d9,c3cc136c9ff5fbe73b2d2f5a3ce33658,ff866b05d11084c5b93697ce6d4cdf7f,5efcc24145ba83a73d84f8b4dbcb72b1,b61c58c22bd2669dc41f2922313c29d4,6301a395b54ced74a4baaf84c01d2e80,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> <!-- werkrevisor-folded --> <details><summary>🟡 werkrevisor YELLOW for <code>ce98a65</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6649">the result for <code>11af83c</code></a></summary> ## 🟡 werkrevisor: YELLOW / HUMAN_REVIEW Change kind: `feature_with_tests`. ### Reasons - **YELLOW** PR too large for an AI-only approval: 2450 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **YELLOW** Sandbox isolation according to Jev: internal/build/build.go (0.86), internal/engine/bwrap.go (0.94); a human checks it, unless you revert the change because the task does not need it - **hint** writing rules violated in the changed docs: README.md:10: more than one sentence on a line; README.md:10: line longer than 128 characters (2249); README.md:20: line longer than 128 characters (141); and 140 more - **hint** 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev - **hint** 1 finding(s) of low severity or rated minor by Jev: README.md:10 - **hint** model recommends merge with caution ### Findings 1. **README formatting bug** at `README.md:10`, severity unknown, counted as medium hint; Jev: real 0.96, severity 1.6 of 3 The new `build` bullet was appended directly to the end of the existing `import`/load/pull/images/rmi line without a newline separator, producing a single very long line that ends with "removes images.- `build` builds ... (bwrap 0.11).". In the rendered Markdown the `build` text is part of the import bullet, not its own list item, so the new build verb is effectively undocumented in the README. The CLI help text and the CHANGELOG entry document it, but the README's bullets under "Semantics" need a newline between "removes images." and "- `build` builds..." so it is a proper list item. <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 107989, calls 1, tokens in 103785 / out 29344 (reasoning included in out), duration 238.1 s, cost 0.0651 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 1, tokens in 3518, cost 0.0001 USD, build file judge `jev-1.13.0` calls 18, tokens in 40919, cost 0.0017 USD, phases: path_judge 1.2 s, context 0.9 s, model_review 235.2 s, finding_judge 0.3 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).<br> Reviewed head `ce98a6560fc6f6afc423277241aeb79c920bdfb0` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1. </sub> </details>
mi added 1 commit 2026-10-20 15:24:02 +02:00
README: build is a bullet of its own
werkator build succeeded after 00:02
werkator/dependencies build succeeded after 00:05
ai-review/assessment AI findings; a developer approval after checking them unlocks the merge; review request failed: POST /repos/mi/werkdock/pulls/14/requested_reviewers failed with HTTP 422
ai-review/gate build failed after 03:34
11af83c9ab
A hint of werkrevisor on PR#14: the bullet stood at the end of the import line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
🟠 werkrevisor ORANGE for 11af83c, superseded by the result for 8dd301e

🟠 werkrevisor: ORANGE / FINDINGS

Change kind: feature_with_tests.

Reasons

  • ORANGE 3 finding(s) that Jev rates as probably real and severe: internal/build/copyinstruction.go:144-155, internal/build/build.go:178-183, internal/cli/build.go:126-135
  • YELLOW PR too large for an AI-only approval: 2450 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • YELLOW Sandbox isolation according to Jev: internal/engine/bwrap.go (0.93); a human checks it, unless you revert the change because the task does not need it
  • YELLOW model reports a security concern: Build context escape: a symlink or symlinked glob beneath CONTEXT can point at a readable host file because validation is lexical while file copying follows links. A malicious checkout could place host secrets in the built image and then exfiltrate them with a network-enabled RUN step.
  • hint writing rules violated in the changed docs: README.md:11: more than one sentence on a line; README.md:11: line longer than 128 characters (1218); README.md:21: line longer than 128 characters (141); and 140 more
  • hint 17 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
  • hint model recommends changes before merge
  • hint model rates the risk as high (advisory: Jev judged every changed file)

Findings

  1. [high] Context Escape at internal/build/copyinstruction.go:144-155, ORANGE, as a line comment

  2. [high] Missing Workdir at internal/build/build.go:178-183, ORANGE, as a line comment

  3. [high] Scratch Failure at internal/cli/build.go:126-135, ORANGE, as a line comment

Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 107281, calls 1, tokens in 103148 / out 24549 (reasoning included in out), duration 211.5 s, cost 0.0604 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 3, tokens in 5663, cost 0.0002 USD, build file judge `jev-1.13.0` calls 18, tokens in 40919, cost 0.0017 USD, phases: path_judge 1.1 s, context 0.5 s, model_review 209.0 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).
Reviewed head `11af83c9ab2f0500045b1e39c4afcfe4f63d2368` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
<!-- werkrevisor:11af83c9ab2f0500045b1e39c4afcfe4f63d2368 --> <!-- werkrevisor-verdict:ORANGE --> <!-- werkrevisor-blocks:4f41e79771d454bd5f2213aee8a987a5,eafda2ec463af4262d406a2cb171e0f4,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,b2625ae8384c82b801a82b6cbef97e5e,6028d389a45132790d20266e6cd74b5c,d9ee6fec8fa0330b22c52df22d132de9,37a001f5d306b37459a642eaac630a99,accc3ec0a3d50e927d717f2a8682f05b,da5f319db91a51e49a6b21c88f17840f,690fe8d5c8b477656f88804d7a1ac9bc,48ba638de328608a1c4bcdb7bddd7566,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,135caa91eb6b88e57e28cba7c29e3e37,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,900df901cad2eb32989f0e21fc6894d9,c3cc136c9ff5fbe73b2d2f5a3ce33658,ff866b05d11084c5b93697ce6d4cdf7f,5efcc24145ba83a73d84f8b4dbcb72b1,b61c58c22bd2669dc41f2922313c29d4,6301a395b54ced74a4baaf84c01d2e80,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> <!-- werkrevisor-folded --> <details><summary>🟠 werkrevisor ORANGE for <code>11af83c</code>, superseded by <a href="http://git.javagil.de/mi/werkdock/pulls/14#issuecomment-6664">the result for <code>8dd301e</code></a></summary> ## 🟠 werkrevisor: ORANGE / FINDINGS Change kind: `feature_with_tests`. ### Reasons - **ORANGE** 3 finding(s) that Jev rates as probably real and severe: internal/build/copyinstruction.go:144-155, internal/build/build.go:178-183, internal/cli/build.go:126-135 - **YELLOW** PR too large for an AI-only approval: 2450 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **YELLOW** Sandbox isolation according to Jev: internal/engine/bwrap.go (0.93); a human checks it, unless you revert the change because the task does not need it - **YELLOW** model reports a security concern: Build context escape: a symlink or symlinked glob beneath `CONTEXT` can point at a readable host file because validation is lexical while file copying follows links. A malicious checkout could place host secrets in the built image and then exfiltrate them with a network-enabled `RUN` step. - **hint** writing rules violated in the changed docs: README.md:11: more than one sentence on a line; README.md:11: line longer than 128 characters (1218); README.md:21: line longer than 128 characters (141); and 140 more - **hint** 17 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev - **hint** model recommends changes before merge - **hint** model rates the risk as high (advisory: Jev judged every changed file) ### Findings 1. **[high] Context Escape** at `internal/build/copyinstruction.go:144-155`, ORANGE, as a line comment 2. **[high] Missing Workdir** at `internal/build/build.go:178-183`, ORANGE, as a line comment 3. **[high] Scratch Failure** at `internal/cli/build.go:126-135`, ORANGE, as a line comment <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 107281, calls 1, tokens in 103148 / out 24549 (reasoning included in out), duration 211.5 s, cost 0.0604 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 3, tokens in 5663, cost 0.0002 USD, build file judge `jev-1.13.0` calls 18, tokens in 40919, cost 0.0017 USD, phases: path_judge 1.1 s, context 0.5 s, model_review 209.0 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).<br> Reviewed head `11af83c9ab2f0500045b1e39c4afcfe4f63d2368` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1. </sub> </details>
ai-reviewer left a comment
Collaborator

werkrevisor: ORANGE / FINDINGS for 11af83c9ab2f0500045b1e39c4afcfe4f63d2368.
The result comment of werkrevisor holds the details.

werkrevisor: ORANGE / FINDINGS for `11af83c9ab2f0500045b1e39c4afcfe4f63d2368`. The result comment of werkrevisor holds the details. <!-- werkrevisor-review:11af83c9ab2f0500045b1e39c4afcfe4f63d2368 -->
README.md Outdated
@@ -8,6 +8,8 @@ Semantics — docker-compatible as far as the filesystem-only contract allows (s
- An *image* is a rootfs archive; an *instance* is an unpacked, writable directory tree and corresponds to a docker container.
- `import` takes a plain rootfs archive, as `docker import` does; `load` takes a `docker save` archive or an OCI image layout, flattens the layers (whiteouts honored, paths resolved inside the rootfs as in a chroot) and keeps the OCI config — so a Dockerfile-built image lands with its ENTRYPOINT, CMD, ENV, and WORKDIR ([RFC 0004](docs/rfcs/0004-docker-compatible-image-creation.md)). `load -i … --name` still routes a rootfs archive to `import`, for callers that predate it. `pull` fetches an image from a registry the same way, with the registry side (auth, manifest lists, blobs) delegated to the [crane](https://github.com/google/go-containerregistry) binary — a single unprivileged executable found in `PATH`, beside `werkdock`, or at `WERKDOCK_CRANE`; it reads `~/.docker/config.json` (`DOCKER_CONFIG`) for credentials as docker does. `images` lists one reference per line by default (what a script matches against) and takes docker's `--format`, with `--format table` giving docker's columns; `rmi IMAGE...` removes images.
- `build` builds an image from a Dockerfile on this host — `werkdock build [-f Dockerfile] -t IMAGE CONTEXT`, docker's flags ([RFC 0006](docs/rfcs/0006-dockerfile-builds.md)). `FROM` resolves through the store and pulls on a miss, one writable work tree is created over the base, the steps run against it, and the result is an ordinary flat image. Stage 1 is `FROM`, `RUN`, `COPY` plus the metadata instructions `import -c` already knew; `ARG`, `SHELL`, `ADD` and multi-stage builds name the stage of RFC 0006 that brings them, and `EXPOSE`, `HEALTHCHECK`, `STOPSIGNAL`, `ONBUILD` and BuildKit's `RUN --mount` are refused with the reason at parse time. Werkdock sets the two levers an unprivileged single-uid sandbox needs itself, so the Dockerfile stays portable: `APT::Sandbox::User root` for the duration of the build, and `fakeroot` with `FAKEROOTDONTTRYCHOWN=1` around every `RUN` step — which means **the base image needs `fakeroot` installed**, or a package whose postinst chowns fails. There is **no build cache**: every build starts at `FROM`, because caching a step without a union mount would cost a whole tree copy. That changes with the overlay backend, after the webspaces move to Debian 13 (bwrap 0.11).
Collaborator

Writing rule: more than one sentence on a line (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: more than one sentence on a line** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Writing rule: line longer than 128 characters (1218) (werkrevisor writing rule)

Fix the line, or reply why it stays, and resolve this comment.

**Writing rule: line longer than 128 characters (1218)** (werkrevisor writing rule) Fix the line, or reply why it stays, and resolve this comment.
Collaborator

Fixed in 8dd301e: one sentence per line.

Fixed in `8dd301e`: one sentence per line.
Collaborator

Fixed in 8dd301e: every line at most 128 characters.

Fixed in `8dd301e`: every line at most 128 characters.
claude-code marked this conversation as resolved
@@ -0,0 +175,4 @@
// apply executes one instruction. n counts the instructions after
// FROM, which is what a backend keys its steps by.
func (b *builder) apply(tree Tree, n int, instruction Instruction) error {
if !instruction.changesTree() {
Collaborator

[high] Missing Workdir (werkrevisor)

ORANGE; Jev: real 0.77, severity 3.0 of 3

WORKDIR is applied only to the image configuration, so its directory is not created when no later RUN or COPY touches it. A Dockerfile ending with WORKDIR /app and CMD can therefore commit an image whose normal read-only run fails CheckWorkdir because /app does not exist. Create the directory when applying WORKDIR and test a Dockerfile where it is the last tree-affecting instruction.

**[high] Missing Workdir** (werkrevisor) ORANGE; Jev: real 0.77, severity 3.0 of 3 `WORKDIR` is applied only to the image configuration, so its directory is not created when no later `RUN` or `COPY` touches it. A Dockerfile ending with `WORKDIR /app` and `CMD` can therefore commit an image whose normal read-only run fails `CheckWorkdir` because `/app` does not exist. Create the directory when applying `WORKDIR` and test a Dockerfile where it is the last tree-affecting instruction.
Collaborator

Fixed in 8dd301e: the commit creates the final WORKDIR in the tree, as docker does. Test: TestRunCreatesTheWorkdirOfTheFinishedImage, red before.

Fixed in `8dd301e`: the commit creates the final WORKDIR in the tree, as docker does. Test: `TestRunCreatesTheWorkdirOfTheFinishedImage`, red before.
claude-code marked this conversation as resolved
@@ -0,0 +141,4 @@
if !within(context, candidate) {
return nil, fmt.Errorf("COPY source %s leaves the build context", source)
}
matches, err := filepath.Glob(candidate)
Collaborator

[high] Context Escape (werkrevisor)

ORANGE; Jev: real 0.77, severity 3.0 of 3

Source validation is lexical, but os.Stat and copyFile subsequently follow symlinks. A context entry such as leak -> /home/user/.ssh/id_rsa therefore passes within and copies a readable host file into the image, allowing an untrusted checkout to expose secrets. Resolve or reject symlinks and verify the resolved source remains beneath the context.

**[high] Context Escape** (werkrevisor) ORANGE; Jev: real 0.77, severity 3.0 of 3 Source validation is lexical, but `os.Stat` and `copyFile` subsequently follow symlinks. A context entry such as `leak -> /home/user/.ssh/id_rsa` therefore passes `within` and copies a readable host file into the image, allowing an untrusted checkout to expose secrets. Resolve or reject symlinks and verify the resolved source remains beneath the context.
Collaborator

Fixed in 8dd301e: resolveSources compares the real path of each match (filepath.EvalSymlinks) with the real context and refuses one outside; a link inside the context still copies its target under the link's name. Red/green: TestCopyRefusesASymlinkThatLeavesTheContext (a linked file, a file below a linked directory, a glob) fails without the fix with the host file in the image and passes with it; TestCopyFollowsASymlinkWithinTheContext.

Fixed in `8dd301e`: `resolveSources` compares the real path of each match (`filepath.EvalSymlinks`) with the real context and refuses one outside; a link inside the context still copies its target under the link's name. Red/green: `TestCopyRefusesASymlinkThatLeavesTheContext` (a linked file, a file below a linked directory, a glob) fails without the fix with the host file in the image and passes with it; `TestCopyFollowsASymlinkWithinTheContext`.
claude-code marked this conversation as resolved
@@ -0,0 +123,4 @@
// `docker build` pulls a base it does not have. Without a registry
// client the error names the two verbs that would put the image there.
func resolveBase(st store.Store) func(string) (string, store.ImageMeta, error) {
return func(reference string) (string, store.ImageMeta, error) {
Collaborator

[high] Scratch Failure (werkrevisor)

ORANGE; Jev: real 0.89, severity 3.0 of 3

FROM scratch is sent to the registry on a store miss, although Docker reserves scratch as an empty local base and it cannot be pulled. Valid static-binary or COPY-only scratch builds therefore fail with misleading instructions to pull it. Handle scratch as an empty root filesystem with default metadata and add a regression test.

**[high] Scratch Failure** (werkrevisor) ORANGE; Jev: real 0.89, severity 3.0 of 3 `FROM scratch` is sent to the registry on a store miss, although Docker reserves `scratch` as an empty local base and it cannot be pulled. Valid static-binary or COPY-only scratch builds therefore fail with misleading instructions to pull it. Handle `scratch` as an empty root filesystem with default metadata and add a regression test.
Collaborator

Fixed in 8dd301e: FROM scratch starts from an empty directory with empty metadata, without lookup or pull; README and CHANGELOG say so. Test: TestRunBuildsFromScratchWithoutAskingForIt, red before.

Fixed in `8dd301e`: `FROM scratch` starts from an empty directory with empty metadata, without lookup or pull; README and CHANGELOG say so. Test: `TestRunBuildsFromScratchWithoutAskingForIt`, red before.
claude-code marked this conversation as resolved
mi added 1 commit 2026-10-20 15:32:47 +02:00
COPY refuses a symlink out of the context; the finished image has its WORKDIR; FROM scratch is an empty base
werkator build succeeded after 00:04
werkator/dependencies build succeeded after 00:06
ai-review/assessment human review required; a human approval unlocks the merge; review request failed: POST /repos/mi/werkdock/pulls/14/requested_reviewers failed with HTTP 422
ai-review/gate build failed after 03:58
8dd301eb15
Findings of werkrevisor on PR#14:
- a context entry such as leak -> ~/.ssh/id_rsa passed the lexical check and copied a host file into the image;
  the sources are now compared by their real paths (red/green: TestCopyRefusesASymlinkThatLeavesTheContext),
- a WORKDIR that no later step touched was missing in the image, so a run of it failed,
- FROM scratch went to the registry.
The README's build bullet has one sentence per line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator

🟡 werkrevisor: YELLOW / HUMAN_REVIEW

Change kind: feature_with_tests.

Reasons

  • YELLOW PR too large for an AI-only approval: 2576 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself
  • YELLOW Sandbox isolation according to Jev: internal/build/build.go (0.85), internal/engine/bwrap.go (0.93); a human checks it, unless you revert the change because the task does not need it
  • hint writing rules violated in the changed docs: README.md:34: line longer than 128 characters (141); README.md:44: line longer than 128 characters (613); README.md:47: line longer than 128 characters (223); and 138 more
  • hint 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev
  • hint 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (all full files together would exceed [review] max_full_files_chars = 160000, and no excerpt fits the limits either): internal/store/stage_test.go (advisory: Jev judged every changed file)
  • hint 1 finding(s) of low severity or rated minor by Jev: internal/cli/cli.go:20
  • hint model recommends merge with caution

Findings

  1. Version bump inconsistency at internal/cli/cli.go:20, severity unknown, counted as medium, hint, as a line comment
Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 109136, calls 1, tokens in 104884 / out 18038 (reasoning included in out), duration 236.2 s, cost 0.0531 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 1, tokens in 1615, cost 0.0001 USD, excerpt selection `openrouter/openai/gpt-6-luna` calls 1, tokens in 3177 / out 61, cost 0.0004 USD (reported), build file judge `jev-1.13.0` calls 18, tokens in 42524, cost 0.0018 USD, phases: path_judge 1.2 s, context 3.8 s, model_review 229.8 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).
Reviewed head `8dd301eb15e289bfba1e5cafc71e66c1ed562497` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
<!-- werkrevisor:8dd301eb15e289bfba1e5cafc71e66c1ed562497 --> <!-- werkrevisor-verdict:YELLOW --> <!-- werkrevisor-blocks:5e41cc30e92f6f92b532d0a85b2a720e,0c5d8d6adc71edb051587a298a0dd2b8,9fed444615e9057d14705117dc945117,4e1914e2b57fc11ab38cf13768a37f49,c0fc5732f94ca3faf6a03c62e8fa630b,6d94ac1fb5c00a597400fa54f5b04445,87218850f543d6964902cd88a8ab7488,b2625ae8384c82b801a82b6cbef97e5e,6028d389a45132790d20266e6cd74b5c,407bb84ea589bb634a6f7a6ce35403b2,05e3b561146cdf893e803e32709eb38e,accc3ec0a3d50e927d717f2a8682f05b,2e41032eb63a935c64e4d8800e9be290,690fe8d5c8b477656f88804d7a1ac9bc,48ba638de328608a1c4bcdb7bddd7566,2684e4ea57506bbbd33c7c75ac04f13a,ae48afa23aa3c4c37baf20cad7ddf77d,edd6ec41beb2ac5f01e2d87e31934aab,135caa91eb6b88e57e28cba7c29e3e37,2b92101e61e79ba35f33004cdba63738,cae716e51d55ee608762dcc6d666f920,080c153ef40c73181805eee67aa011ab,900df901cad2eb32989f0e21fc6894d9,c3cc136c9ff5fbe73b2d2f5a3ce33658,ff866b05d11084c5b93697ce6d4cdf7f,5efcc24145ba83a73d84f8b4dbcb72b1,b61c58c22bd2669dc41f2922313c29d4,6301a395b54ced74a4baaf84c01d2e80,2548f5c759482fb731bbde1c45c0a38c,4d493756ee23a00772b03859a1a2b404,603f6dade0c511acf8b7c3c716ad09ea,4dacc16624e5e0c34cc6778456d61feb,609136fc8ddb50bdacbdd884308913d4,4429c3f2b72a7ee7219ac9452e16f2f5,f2e945afdd370b6beef9e8b7dcf54e4c,4b1e63b6fed8967c95bbcaa7835082a1,3b32ddecb03c367e8724ece9836440f8 --> ## 🟡 werkrevisor: YELLOW / HUMAN_REVIEW Change kind: `feature_with_tests`. ### Reasons - **YELLOW** PR too large for an AI-only approval: 2576 changed lines (docs and mechanical files not counted) exceed [size] max_changed_lines = 800; please review it yourself - **YELLOW** Sandbox isolation according to Jev: internal/build/build.go (0.85), internal/engine/bwrap.go (0.93); a human checks it, unless you revert the change because the task does not need it - **hint** writing rules violated in the changed docs: README.md:34: line longer than 128 characters (141); README.md:44: line longer than 128 characters (613); README.md:47: line longer than 128 characters (223); and 138 more - **hint** 16 changed file(s) that neither weaken the checks, touch security nor show a risk category of jvm-spring-backend according to Jev - **hint** 1 changed file was given to the AI only as diff, not in full, so it could not check it against the rest of its file (all full files together would exceed [review] max_full_files_chars = 160000, and no excerpt fits the limits either): internal/store/stage_test.go (advisory: Jev judged every changed file) - **hint** 1 finding(s) of low severity or rated minor by Jev: internal/cli/cli.go:20 - **hint** model recommends merge with caution ### Findings 1. **Version bump inconsistency** at `internal/cli/cli.go:20`, severity unknown, counted as medium, hint, as a line comment <sub> Usage: model `openrouter/minimax/minimax-m3`, LLM done, tokens counted before the call 109136, calls 1, tokens in 104884 / out 18038 (reasoning included in out), duration 236.2 s, cost 0.0531 USD (reported), classifier `jev-latest` calls 0, tokens in 0, cost 0.0000 USD, finding judge `jev-1.13.0` calls 1, tokens in 1615, cost 0.0001 USD, excerpt selection `openrouter/openai/gpt-6-luna` calls 1, tokens in 3177 / out 61, cost 0.0004 USD (reported), build file judge `jev-1.13.0` calls 18, tokens in 42524, cost 0.0018 USD, phases: path_judge 1.2 s, context 3.8 s, model_review 229.8 s, finding_judge 0.2 s, config: defaults + host + repository (.werkrevisor.toml at main c30be8b).<br> Reviewed head `8dd301eb15e289bfba1e5cafc71e66c1ed562497` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1. </sub>
ai-reviewer left a comment
Collaborator

werkrevisor: YELLOW / HUMAN_REVIEW for 8dd301eb15e289bfba1e5cafc71e66c1ed562497.
The result comment of werkrevisor holds the details.

werkrevisor: YELLOW / HUMAN_REVIEW for `8dd301eb15e289bfba1e5cafc71e66c1ed562497`. The result comment of werkrevisor holds the details. <!-- werkrevisor-review:8dd301eb15e289bfba1e5cafc71e66c1ed562497 -->
@@ -18,3 +18,3 @@
// bumps it here and titles its CHANGELOG section with it; there is no
// separate release step.
var Version = "0.12.1"
var Version = "0.13.0"
Collaborator

Version bump inconsistency, severity unknown, counted as medium (werkrevisor)

hint; Jev: real 0.62, severity 1.1 of 3

The PR title announces version "(0.12.0)" but the actual code version was bumped from 0.12.1 to 0.13.0 and the CHANGELOG section is titled 0.13.0. The title is stale/misleading relative to the implementation; per AGENTS.md the version stands in internal/cli/cli.go and titles the CHANGELOG section, so the title should be brought into line.

**Version bump inconsistency**, severity unknown, counted as medium (werkrevisor) hint; Jev: real 0.62, severity 1.1 of 3 The PR title announces version "(0.12.0)" but the actual code version was bumped from 0.12.1 to 0.13.0 and the CHANGELOG section is titled 0.13.0. The title is stale/misleading relative to the implementation; per AGENTS.md the version stands in `internal/cli/cli.go` and titles the CHANGELOG section, so the title should be brought into line.
Collaborator

Right: the rebase onto main after PR#15 (0.12.1) raised the version to 0.13.0. The title is the author's; mi changes it to "(0.13.0)" before the merge.

Right: the rebase onto main after PR#15 (0.12.1) raised the version to 0.13.0. The title is the author's; mi changes it to "(0.13.0)" before the merge.
claude-code marked this conversation as resolved
mi merged commit 7504b97235 into main 2026-10-20 15:41:48 +02:00
mi deleted branch feature/dockerfile-builds 2026-10-20 15:41:48 +02:00
Sign in to join this conversation.
No Reviewers
No labels
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mi/werkdock#14