Skip to content

[3400 by Claude Opus 5.5] Fix #3399: the published agent image cannot be unpacked on hosts that cannot apply overlayfs opaque whiteouts (reported on OrbStack, Ubuntu 24.04.5 x86_64, Docker 29.1.3 with the containerd overlayfs image store: `failed to convert whiteout file... - #3402

Open
propr-dev[bot] wants to merge 1 commit into
mainfrom
3400/claude-opus-5-5-fix-3399-the-published-ag-20261031-1127-4wc

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

AI Implementation Summary

Closes #3400

Branch: 3400/claude-opus-5-5-fix-3399-the-published-ag-20261031-1127-4wc
Commits: ✅ Changes committed (28fef58)


AI Processing Completed

Execution Details:

Summary:
I've changed Dockerfile.agent so the agent image should ship with no overlayfs whiteouts. I could not build the image here: this container has no Docker and can't create user namespaces. So I haven't run scripts/build-images.sh --only agent, the existing agent smoke checks, or a restricted-snapshotter reproduction. The real build happens in the restricted-network smoke workflow, which runs on this PR because Dockerfile.agent changed and now includes the new whiteout check.

What I checked against the published 0.9.0 image. I downloaded propr/agent:0.9.0 (sha256:22b319a7721b…) from Docker Hub and listed every layer:

  • Layer 5 (the big apt-get install step) has 153 whiteouts: 152 .wh..wh..opq plus one plain var/lib/apt/lists/.wh.auxfiles. No other layer has any.
  • None of the 152 opaque directories exists in the node base layers, which confirms the issue's diagnosis.
  • I merged the 12 layers that the fix flattens, the same way COPY --from=agent-base-build / / does. That one layer has 0 whiteouts, and the new check passes on it combined with the real 0.9.0 upper layers (18 layers).

Size. In that simulation the flattened layer is 223.3 MB compressed, against 229.3 MB for the 12 layers it replaces: about 6 MB smaller. The only cost is that the image no longer shares layers with a locally pulled node image.

The fix

  • Dockerfile.agent: the old agent-base steps now live in an internal stage, agent-base-build. A new agent-base stage starts FROM --platform=${AGENT_PLATFORM} scratch and copies that stage's whole filesystem in. It then re-declares the base image's settings exactly: PATH, NODE_VERSION, YARN_VERSION, ENTRYPOINT ["docker-entrypoint.sh"], CMD ["node"], plus the existing NODE_OPTIONS, USER and WORKDIR. All later stages are unchanged and only add paths.
  • Platform pin on the scratch stage: this is needed. Custom bundle builds on users' hosts don't pass --platform, so on an arm64 host a bare FROM scratch would label the amd64 contents as arm64.
  • New pin, for your call: the Node base is now node:22.23.4-bookworm-slim (ARG AGENT_NODE_VERSION, ARG AGENT_YARN_VERSION). Today that tag has the same digest as node:22-bookworm-slim. The build fails if the base image's versions or node --version disagree with the pins, so the re-declared values can't drift. The downside is that Node patch releases now need a manual bump instead of arriving automatically.
  • Unchanged: the CLI version matrix, the package and checksum pins, and the bundle content-hash inputs. The agent bundle tag changes only because Dockerfile.agent's content changed.

The check

  • scripts/check-image-whiteouts.sh (new): runs docker save IMAGE, or reads a saved tarball with --archive FILE, and fails if any layer contains a file whose name starts with .wh.. It handles both docker save formats, including compressed layers. On the 0.9.0 archive it fails with "layer 5: 153 whiteout entries (152 opaque)".
  • Wired into:
    • the release smoke (smoke-test-images.sh)
    • the preview agent smoke, where no-overlay-whiteouts is now a required evidence check in preview-runtime-images.mjs
    • the restricted-network smoke workflow, for images built from the checkout only, since earlier published images still have whiteouts
    • the shellcheck list
  • Tests: a new test/checkImageWhiteouts.test.mjs (7 cases on synthetic archives), plus new assertions in test/agentDockerfileSupplyChain.test.ts and an updated test/agentVersionManagement.test.ts. Against the old Dockerfile the 3 new supply-chain assertions fail, as they should. The related suites I ran pass (preview agent and runtime, workflow, image smoke, bundle content files, build-images, release validation, managed-agent pin), shellcheck is clean, and npm run release:verify passes.
  • CHANGELOG: added a ### Fixed entry under [Unreleased].

Workaround for #3399, for the PR body:

Images up to 0.9.0 carry opaque whiteouts that hosts unable to set trusted overlay xattrs cannot unpack. Until 0.9.1, either switch Docker back to the classic storage driver by turning off the containerd image store ("features": {"containerd-snapshotter": false} in /etc/docker/daemon.json, or untick "Use containerd for pulling and storing images" in Docker Desktop, then restart Docker and pull again), or use OrbStack's own Docker engine instead of a Docker install inside an OrbStack Linux machine.

No visual previews, since nothing here is visible in a UI.

Detailed Logs:

  • Conversation: 164 messages
  • Session: 6615ea4f-05b3-4a69-b9a5-7abafe973d74

Log files stored at:

  • conversation: /usr/src/app/logs/executions/issue-3400-2026-10-31T11-42-24-403Z-conversation.json
  • output: /usr/src/app/logs/executions/issue-3400-2026-10-31T11-42-24-403Z-output.txt
Latest Conversation Messages
ASSISTANT: [content unavailable]

ASSISTANT: I've changed `Dockerfile.agent` so the agent image should ship with no overlayfs whiteouts. **I could not build the image here**: this container has no Docker and can't create user namespaces. So I ha...


This PR was created automatically by ProPR after processing issue #3400.


💡 Need changes?

Comment on this PR to request refinements — the AI agent monitors comments and will update the implementation based on your feedback. Keep iterating until you're satisfied!

…o overlayfs whiteouts. I could not build the image here: this container has no Docker and can't create user namespaces. So I haven't run `scripts/build-images.sh --only agent`, the existing agent smoke checks, or a restricted-snapshotter reproduction. The real build happens in the restricted-network smoke workflow, which runs on this PR because `Dockerfile.agent` changed and now includes the new whiteout check.

What I checked against the published 0.9.0 image. I downloaded `propr/agent:0.9.0` (sha256:22b319a7721b…) from Docker Hub and listed every layer:
- Layer 5 (the big `apt-get install` step) has 153 whiteouts: 152 `.wh..wh..opq` plus one plain `var/lib/apt/lists/.wh.auxfiles`. No other layer has any.
- None of the 152 opaque directories exists in the node base layers, which confirms the issue's diagnosis.
- I merged the 12 layers that the fix flattens, the same way `COPY --from=agent-base-build / /` does. That one layer has 0 whiteouts, and the new check passes on it combined with the real 0.9.0 upper layers (18 layers).

Size. In that simulation the flattened layer is 223.3 MB compressed, against 229.3 MB for the 12 layers it replaces: about 6 MB smaller. The only cost is that the image no longer shares layers with a locally pulled `node` image.

The fix
- `Dockerfile.agent`: the old `agent-base` steps now live in an internal stage, `agent-base-build`. A new `agent-base` stage starts `FROM --platform=${AGENT_PLATFORM} scratch` and copies that stage's whole filesystem in. It then re-declares the base image's settings exactly: `PATH`, `NODE_VERSION`, `YARN_VERSION`, `ENTRYPOINT ["docker-entrypoint.sh"]`, `CMD ["node"]`, plus the existing `NODE_OPTIONS`, `USER` and `WORKDIR`. All later stages are unchanged and only add paths.
- Platform pin on the scratch stage: this is needed. Custom bundle builds on users' hosts don't pass `--platform`, so on an arm64 host a bare `FROM scratch` would label the amd64 contents as arm64.
- New pin, for your call: the Node base is now `node:22.23.4-bookworm-slim` (`ARG AGENT_NODE_VERSION`, `ARG AGENT_YARN_VERSION`). Today that tag has the same digest as `node:22-bookworm-slim`. The build fails if the base image's versions or `node --version` disagree with the pins, so the re-declared values can't drift. The downside is that Node patch releases now need a manual bump instead of arriving automatically.
- Unchanged: the CLI version matrix, the package and checksum pins, and the bundle content-hash inputs. The agent bundle tag changes only because `Dockerfile.agent`'s content changed.

The check
- `scripts/check-image-whiteouts.sh` (new): runs `docker save IMAGE`, or reads a saved tarball with `--archive FILE`, and fails if any layer contains a file whose name starts with `.wh.`. It handles both `docker save` formats, including compressed layers. On the 0.9.0 archive it fails with "layer 5: 153 whiteout entries (152 opaque)".
- Wired into:
  - the release smoke (`smoke-test-images.sh`)
  - the preview agent smoke, where `no-overlay-whiteouts` is now a required evidence check in `preview-runtime-images.mjs`
  - the restricted-network smoke workflow, for images built from the checkout only, since earlier published images still have whiteouts
  - the shellcheck list
- Tests: a new `test/checkImageWhiteouts.test.mjs` (7 cases on synthetic archives), plus new assertions in `test/agentDockerfileSupplyChain.test.ts` and an updated `test/agentVersionManagement.test.ts`. Against the old Dockerfile the 3 new supply-chain assertions fail, as they should. The related suites I ran pass (preview agent and runtime, workflow, image smoke, bundle content files, build-images, release validation, managed-agent pin), shellcheck is clean, and `npm run release:verify` passes.
- CHANGELOG: added a `### Fixed` entry under `[Unreleased]`.

Workaround for #3399, for the PR body:
> Images up to 0.9.0 carry opaque whiteouts that hosts unable to set trusted overlay xattrs cannot unpack. Until 0.9.1, either switch Docker back to the classic storage driver by turning off the containerd image store (`"features": {"containerd-snapshotter": false}` in `/etc/docker/daemon.json`, or untick "Use containerd for pulling and storing images" in Docker Desktop, then restart Docker and pull again), or use OrbStack's own Docker engine instead of a Docker install inside an OrbStack Linux machine.

No visual previews, since nothing here is visible in a UI.
@integry

integry commented Oct 31, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR addresses the reported unpack failure by flattening the agent’s base filesystem and checking every resulting image layer for whiteouts. It is ready to merge within the reviewed scope, conditional on the remaining Validate Changes check passing.

✅ Preserved runtime configuration — The scratch stage restores Node’s environment and entrypoint configuration, retains the node user and workspace, and verifies the pinned Node/Yarn versions before flattening.

✅ Artifact-level enforcement — Release, preview, and restricted-network smoke paths invoke the whiteout checker. Preview evidence also requires the new check before packaging.

✅ Focused regression coverage — Added tests cover opaque and deletion whiteouts, compressed layers, both archive layouts, and rejection of unsafe manifest paths.

This was a static review; no commands were run. The supplied checks report 27 passed, none failed, and one pending, including a passed restricted-network container smoke. The original issue excerpt is truncated, so assessment of its requirements is limited to the visible objective; the supplied diff is complete.

Merge blockers

No merge blockers.

Suggestions

These are optional follow-ups and are not sent to /fix.

S1: 🟢 Preserve diagnostics for large failures

In scripts/check-image-whiteouts.sh:97, the diagnostic pipeline uses printf | head -n 20 under pipefail. With sufficiently large whiteout output, head can close the pipe early, causing SIGPIPE and terminating the script before its final summary or inspection of subsequent layers. The image still correctly fails validation, so this is non-blocking. A filter that consumes all input while printing only the first 20 lines would preserve the intended diagnostics; a large fixture could cover that behavior.

Score

The implementation directly addresses the visible objective, preserves the relevant runtime contracts, and adds meaningful artifact validation. Current checks support merge readiness, subject to the pending validation completing successfully.

Score: 9/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 43s
  • Tokens: 54,816 (54,240 in / 576 out)
  • Cost: $0.46

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix S1

🤖 Review by ProPR

@integry

integry commented Oct 31, 2026

Copy link
Copy Markdown
Owner

/fix
Empirical check before merge: this doesn't pass its own gate yet. I built the agent image from main + this PR with scripts/build-images.sh --only agent (tag bundle-bbb78a107626-4b02a2). scripts/check-image-whiteouts.sh fails, and an independent docker save + tar scan finds 13 .wh. entries across 19 layers (down from 152, but not zero):

  • layers 2–5, COPY --from=… of @anthropic-ai, @openai, opencode-ai, agent-tank into /usr/local/lib/node_modules/...: one .wh..wh..opq each
  • layer 10, COPY /opt/uv: opt/uv/.wh..wh..opq
  • layer 11, COPY --chown=node:node /home/node/.local: home/node/.local/.wh..wh..opq
  • layer 13, COPY scripts/extension-stdio-bridge.mjs /opt/propr/…: opt/propr/.wh..wh..opq
  • layer 16, the final cleanup RUN: root/.cache, root/.codex, root/.config, root/.local, root/.vibe, tmp/opencode, each with .wh..wh..opq

So this builder's differ marks every newly created directory opaque, including every COPY destination, not only directories it replaces. Flattening only the base can't reach zero. Make the entire shipped image a single layer: keep the current stages as a build stage, then end with FROM scratch + COPY --from=<full final stage> / / and re-declare all image config exactly (ENV including PATH/NODE_*/NODE_OPTIONS, USER, WORKDIR, ENTRYPOINT/CMD, labels). With no lower layer there's nothing to white out. Keep the content-hash, supply-chain and version tests green, and keep the size bounded (the current build is 2.48 GB, same as 0.9.0). Before pushing, actually build with scripts/build-images.sh --only agent, run scripts/check-image-whiteouts.sh against the built image and paste the passing output, and confirm claude, codex, agy, opencode, vibe, agent-tank, gh, git and rg all resolve and the entrypoints still work.

@propr-dev

propr-dev Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor Author

ℹ️ No review findings or suggestions were selected.

No files were changed because this /fix command resolved to no review record.

Name at least one F# finding or S# suggestion from a current review comment, for example /fix F20 S3. Use /fix all to request every pending finding and suggestion. Any text after the identifiers, and every line below the command, is passed through as instructions.

View Task Execution


Processing comment ID: 6546132732✓

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant