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). fakerootalone 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.
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)
⚠️ 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
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
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
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>
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 -->
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).
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.
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.
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>
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>
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
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
Build probe does not exercise root writability at internal/doctor/doctor.go:108-135, severity unknown, counted as medium, ORANGE, as a line comment
resolvConf bind assumes base has the file at internal/build/build.go:254-259, severity unknown, counted as medium, YELLOW, as a line comment
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>
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 -->
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.
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.
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`.
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.
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`.
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
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>
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
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>
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 -->
- 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).
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.").
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`.
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.
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
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
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>
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
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
[high] Context Escape at internal/build/copyinstruction.go:144-155, ORANGE, as a line comment
[high] Missing Workdir at internal/build/build.go:178-183, ORANGE, as a line comment
[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>
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 -->
@@ -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).
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.
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.
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`.
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.
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.
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
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>
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
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>
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 -->
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.
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 main2026-10-20 15:41:48 +02:00
mi
deleted branch feature/dockerfile-builds2026-10-20 15:41:48 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
RFC 0004 had ruled
buildout as "not planned: needs writable rootfs snapshots per step andpullfor FROM". Both halves expired:pullarrived 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:trixierootfs with bwrap 0.11.1 and a0 <uid> 1map — every number in the RFC was taken, not reasoned about:--bindinstead of--ro-bind) works unprivileged.CapEffis0000000000000000:mount -t tmpfsandmknodare 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.apt-get updatedies on apt's_aptuser (seteuid 42 failed,Method http has died unexpectedly);APT::Sandbox::User rootfixes it.apt-get install nginxstill fails (chown: changing ownership of '/var/log/nginx': Invalid argument, dpkg exit 100).fakerootalone does not help — libfakeroot is demonstrably active and itschownwrapper still reports the real syscall's EINVAL.fakerootplusFAKEROOTDONTTRYCHOWN=1does.--overlay-srcstacks, 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'swork/is left atd---------, and bwrap refuses overlay when installed setuid./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 aRunSpec's root fields. Everything else — parser, instructions, COPY, multi-stage, CLI, uid levers, commit — is identical in both worlds.rename, no cache. It stays after the Debian 13 upgrade, as the fallbackdoctorpicks when bwrap is too old or setuid, and for single-shot builds.The warning that belongs with this, in the RFC and as a comment in the code:
store.copyTreehardlinks so a second tag costs no data — there both copies are read-only. For a build tree it is unusable, because aRUNstep writing in place through such a hardlink would modify the base image.Stage 1, implemented
werkdock build [-f Dockerfile] -t IMAGE CONTEXTwith docker's flags.FROM,RUN,COPYplus the metadata instructionsimport -calready knew, through the samestore.ApplyChange. Staging and commit reuse the mechanicsimporthas had since 0.11.0 (<image>.tmp, the per-imageflock,rename), now exported asstore.Staging.Werkdock sets both uid levers itself so the Dockerfile stays portable; the base image needs
fakerootinstalled, andbuildsays so up front when it has not.engine.RunSpecgainedWritableRoot, and a writable-root sandbox gets no implicit tmpfs over/tmp,/root,/run— with one, what aRUNstep writes there would vanish in the next step while docker keeps it in the image.Refused loudly rather than ignored:
EXPOSE,HEALTHCHECK,STOPSIGNAL,ONBUILD,MAINTAINERat parse time;ARG,SHELL,ADDand multi-stage named as stages 2 and 3; BuildKit'sRUN --mountwith the CapEff measurement as the reason; and the dozendocker buildflags werkdock cannot keep the promise of.Verified
go test ./...,go vet ./...,gofmt -l .— green and empty.FROM/WORKDIR/COPY/RUN/CMDand then runs the result; it skips without bwrap/userns like the existing ones.WERKDOCK_TEST_BASE_ROOTFS). Run here:nginx-commonconfigures,dpkg-queryin the built image reportsinstall ok installed, the apt fragment is gone from the result.importa 143 MBdebian:trixiebase with fakeroot,build -t probe:1withapt-get install -y nginx jqin the Dockerfile — 6.8 s — thenrun --rm probe:1prints what itsRUNsteps and itsCOPYproduced.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, andADDfrom a URL.🤖 Generated with Claude Code
⚠️ werkrevisor ERROR for
646179c, superseded by the result for405f02b⚠️ 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 ORANGE for
646179c, superseded by the result for405f02b🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
feature_with_tests.Reasons
Findings
-
-
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.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 commentBackend name hardcoded in doctor at
internal/doctor/doctor.go:133-136, severity unknown, counted as medium, hint, as a line commentReviewed head `646179c2a7f2a215c48f7923505e3e0b550a8d15` against base `ea30cbe79c10e3ef201e36c8e703ed7a5a0b4641`, werkrevisor 0.54.1.
werkrevisor: ORANGE / FINDINGS for
646179c2a7f2a215c48f7923505e3e0b550a8d15.The result comment of werkrevisor holds the details.
@@ -12,0 +12,4 @@## 0.12.0 (2026-10-17)Theme: the verb that was "not planned". `werkdock build` builds an image froma Dockerfile on the target itself — no second machine, no upload. RFC 0004Writing rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -12,0 +24,4 @@`FROM` resolves through the store and pulls on a miss, one writable worktree is created over the base, the steps run against it, and the result isan ordinary flat werkdock image — `images`, `inspect`, `rmi`, `run` and`--mount type=image` apply to it unchanged. Staging is the mechanismWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -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 parsedby one piece of code. Line continuations with `\`, comments and blankWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -12,0 +35,4 @@by one piece of code. Line continuations with `\`, comments and blanklines are handled; `RUN` takes the shell and the exec form.- **Both uid levers are werkdock's business, not the Dockerfile's**, so thesame Dockerfile still builds under docker. Measured on a Debian trixieWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -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.** TheWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -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.** Thecopy backend (a real full copy of the base, root bound read-write) works onthe bwrap 0.8 the webspaces run today. The overlay backend is designed, notWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -12,0 +50,4 @@the bwrap 0.8 the webspaces run today. The overlay backend is designed, notwritten: `--overlay-src` stacks unprivileged on bwrap 0.11 and its upperdirectory is already a layer diff — measured, including deletions appearingas character devices 0:0, which is what OCI writes as `.wh.NAME`. It isWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -12,0 +52,4 @@directory is already a layer diff — measured, including deletions appearingas character devices 0:0, which is what OCI writes as `.wh.NAME`. It iswhat the per-step cache waits for; without a union mount a cached stepwould cost a whole tree copy. The copy backend stays as the fallback.Writing rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -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 writableWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -12,0 +62,4 @@instead of `--ro-bind`. Only build steps set it. A sandbox with a writableroot also gets **no implicit tmpfs** over `/tmp`, `/root` and `/run` — itdoes not need one, and with one, what a `RUN` step writes there would begone in the next step while docker keeps it in the image. Runs areWriting rule: more than one sentence on a line (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
d4cb387: the section 0.12.0 has one sentence per line, its wording unchanged.@@ -0,0 +311,4 @@return nil}func (b *builder) removeAptFragment(rootfs string) error {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
removeAptFragmentcallsos.Remove(target)unconditionally when the build wrote the fragment. If a laterRUNstep — orapt-getitself, or a postinst that cleans/etc/apt/apt.conf.d/— deletes the file, the build fails at commit with the crypticremove /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: ignoreos.IsNotExist(err)fromos.Remove(or re-stat the file and skip the remove when absent).Fixed in
405f02b:removeAptFragmentignoresos.IsNotExist;TestRunToleratesAStepThatRemovedTheAptFragmentfails without the fix and passes with it.@@ -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 isBackend 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, whilebuild.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 andSelect()starts returning"overlay", doctor will still printcopy backenduntil the constant is updated by hand. Doctor should callbuild.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.Fixed in
405f02b: doctor reads the name frombuild.Select(); the constantBuildBackendis gone.🟡 werkrevisor YELLOW for
405f02b, superseded by the result for26225c2🟡 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.
d4cb387abdto26225c245c🟠 werkrevisor ORANGE for
26225c2, superseded by the result for9e4ba14🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
feature_with_tests.Reasons
Findings
-
-
-
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 mainBuild probe does not exercise root writability at
internal/doctor/doctor.go:108-135, severity unknown, counted as medium, ORANGE, as a line commentresolvConf bind assumes base has the file at
internal/build/build.go:254-259, severity unknown, counted as medium, YELLOW, as a line commentrunCommand reuses CMD parsing at
internal/build/build.go:238-247, severity unknown, counted as medium, hint, as a line commentc30be8b).Reviewed head `26225c245cfc5e09e4a685c3e4049b6d8d5a6240` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
werkrevisor: ORANGE / FINDINGS for
26225c245cfc5e09e4a685c3e4049b6d8d5a6240.The result comment of werkrevisor holds the details.
@@ -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) {runCommand reuses CMD parsing, severity unknown, counted as medium (werkrevisor)
hint; Jev: real 0.84, severity 1.5 of 3
runCommandparses RUN's exec form by feeding the args throughstore.ApplyChange(&config, "CMD "+args). When the JSON is malformed the returned error references CMD, not RUN, so aRUN ["bad"yields a confusing "CMD ..." message even though the user wrote RUN. The function is only used for RUN.Fixed in
9e4ba14: the error starts withRUN <args>:and no longer names CMD. Test:TestRunCommandNamesRunInAParseError.@@ -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 {resolvConf bind assumes base has the file, severity unknown, counted as medium (werkrevisor)
YELLOW; Jev: real 0.48, severity 2.8 of 3
resolvConfMountchecks only the host's/etc/resolv.conf, not the base image's. When the base lacks it (e.g.FROM scratch),EnsureMountpointspre-creates an empty file at/etc/resolv.confin 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.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.@@ -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) {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
/tmpwhile 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.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-bindit fails ("Read-only file system"), checked by hand. Tests:TestBuildProbeWritesThroughTheRootBindBelowTheTarget,TestBuildProbeFailsWithoutAPlaceBelowTheTarget.🟠 werkrevisor ORANGE for
9e4ba14, superseded by the result force98a65🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
feature_with_tests.Reasons
Findings
- Doc/code contradiction at
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 mainREADME.md:10, severity unknown, counted as medium, ORANGE, as a line commentc30be8b).Reviewed head `9e4ba14371625257da3d33407777eb9e51baa0af` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
werkrevisor: ORANGE / FINDINGS for
9e4ba14371625257da3d33407777eb9e51baa0af.The result comment of werkrevisor holds the details.
@@ -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).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; aRUN --mount=...line is split into keyword="RUN" and args="--mount=... ..." and is executed as a shell command viarunCommand, 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.").Fixed in
ce98a65: the parser refusesRUN --mount,--networkand--securityat parse time with the reason, as README, help and CHANGELOG say; cases inTestParseRefusesWithTheReason.@@ -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.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: line longer than 128 characters (500) (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
ce98a65: one sentence per line.Fixed in
ce98a65: every line of the paragraph is at most 128 characters.🟡 werkrevisor YELLOW for
ce98a65, superseded by the result for11af83c🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind:
feature_with_tests.Reasons
Findings
-
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 mainREADME formatting bug at
README.md:10, severity unknown, counted as mediumhint; Jev: real 0.96, severity 1.6 of 3
The new
buildbullet was appended directly to the end of the existingimport/load/pull/images/rmi line without a newline separator, producinga single very long line that ends with "removes images.-
buildbuilds... (bwrap 0.11).". In the rendered Markdown the
buildtext is part ofthe 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 "-
buildbuilds..." so itis a proper list item.
c30be8b).Reviewed head `ce98a6560fc6f6afc423277241aeb79c920bdfb0` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
🟠 werkrevisor ORANGE for
11af83c, superseded by the result for8dd301e🟠 werkrevisor: ORANGE / FINDINGS
Change kind:
feature_with_tests.Reasons
CONTEXTcan 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-enabledRUNstep.Findings
-
-
-
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[high] Context Escape at
internal/build/copyinstruction.go:144-155, ORANGE, as a line comment[high] Missing Workdir at
internal/build/build.go:178-183, ORANGE, as a line comment[high] Scratch Failure at
internal/cli/build.go:126-135, ORANGE, as a line commentc30be8b).Reviewed head `11af83c9ab2f0500045b1e39c4afcfe4f63d2368` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
werkrevisor: ORANGE / FINDINGS for
11af83c9ab2f0500045b1e39c4afcfe4f63d2368.The result comment of werkrevisor holds the details.
@@ -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).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: line longer than 128 characters (1218) (werkrevisor writing rule)
Fix the line, or reply why it stays, and resolve this comment.
Fixed in
8dd301e: one sentence per line.Fixed in
8dd301e: every line at most 128 characters.@@ -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() {[high] Missing Workdir (werkrevisor)
ORANGE; Jev: real 0.77, severity 3.0 of 3
WORKDIRis applied only to the image configuration, so its directory is not created when no laterRUNorCOPYtouches it. A Dockerfile ending withWORKDIR /appandCMDcan therefore commit an image whose normal read-only run failsCheckWorkdirbecause/appdoes not exist. Create the directory when applyingWORKDIRand test a Dockerfile where it is the last tree-affecting instruction.Fixed in
8dd301e: the commit creates the final WORKDIR in the tree, as docker does. Test:TestRunCreatesTheWorkdirOfTheFinishedImage, red before.@@ -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)[high] Context Escape (werkrevisor)
ORANGE; Jev: real 0.77, severity 3.0 of 3
Source validation is lexical, but
os.StatandcopyFilesubsequently follow symlinks. A context entry such asleak -> /home/user/.ssh/id_rsatherefore passeswithinand 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.Fixed in
8dd301e:resolveSourcescompares 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.@@ -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) {[high] Scratch Failure (werkrevisor)
ORANGE; Jev: real 0.89, severity 3.0 of 3
FROM scratchis sent to the registry on a store miss, although Docker reservesscratchas 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. Handlescratchas an empty root filesystem with default metadata and add a regression test.Fixed in
8dd301e:FROM scratchstarts from an empty directory with empty metadata, without lookup or pull; README and CHANGELOG say so. Test:TestRunBuildsFromScratchWithoutAskingForIt, red before.🟡 werkrevisor: YELLOW / HUMAN_REVIEW
Change kind:
feature_with_tests.Reasons
Findings
- Version bump inconsistency at
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 maininternal/cli/cli.go:20, severity unknown, counted as medium, hint, as a line commentc30be8b).Reviewed head `8dd301eb15e289bfba1e5cafc71e66c1ed562497` against base `c30be8b44192e002aa0d85609154283830d93a5a`, werkrevisor 0.54.1.
werkrevisor: YELLOW / HUMAN_REVIEW for
8dd301eb15e289bfba1e5cafc71e66c1ed562497.The result comment of werkrevisor holds the details.
@@ -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"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.goand titles the CHANGELOG section, so the title should be brought into line.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.