Skip to content

fix(sandbox): build the guest image from the office bundle's proven versions - #242

Merged
quickbeard merged 1 commit into
mainfrom
fix/sandbox-office-image
Aug 16, 2026
Merged

quickbeard merged 1 commit into
mainfrom
fix/sandbox-office-image

Conversation

@quickbeard

@quickbeard quickbeard commented Aug 15, 2026 •

Copy link
Copy Markdown
Owner

The sandbox guest image installed the office skills, but not the things they need to actually run. This makes the image build a translation of codev-office-ubuntu-setup.sh instead of a loose re-interpretation of it.

Version drift

install-office.sh apt-installed pandoc and LibreOffice, took whatever the dotnet 8.0 channel currently served, and pip-installed the Python set unpinned from PyPI. Against the bundle the skills are tested with:

Component Image built Bundle pins
pandoc 2.17.1.1 3.10.1
LibreOffice 7.4.7.2 26.2.5
.NET SDK 8.0.424 8.0.423
Python set latest from PyPI pinned wheels

Versions are now read off the bundle's own filenames, so refreshing the bundle moves the image with it rather than requiring a second edit here.

Two components were missing entirely

Worse than drift, and both fatal offline:

  • The offline NuGet feed. The docx skill's CLI restores DocumentFormat.OpenXml and friends from it; a sealed guest has no route to nuget.org, so that skill could not run at all. The setup script fails outright when this feed is absent — the image silently shipped without it.
  • Playwright's Chromium. The pdf skill had the Playwright module but no browser to drive, so cover rendering had nothing to render with.

Both now ship, with the NuGet config seeded through /etc/skel (nuget.org cleared, offline feed only) so every fresh sandbox home gets it.

Cross-arch rule

The bundle's binaries are x86_64-only, but every version it pins is obtainable for arm64: LibreOffice's aarch64 deb set, pandoc's arm64 deb, dotnet-install --version 8.0.423 --architecture arm64, and PyPI under a constraints file generated from the bundle's own cp311 wheels. So amd64 uses the bundle's bits verbatim, arm64 fetches the same versions arch-correct, and a version that cannot be matched on an arch is a hard build failure, never a silent substitution.

Both wheel paths were verified before building: the fully-offline amd64 install resolves all 36 packages from the bundle alone, and the constrained arm64 install produces an identical version set. Generating those constraints caught a real drift — magika ships a py3-none-manylinux_x86_64 wheel, so a naive cp311/none-any filter missed it and arm64 resolved 0.6.3 against the bundle's 0.6.2.

Two defects found by verifying the built image

LibreOffice was installed but could not execute. The TDF debs declare almost no dependencies, so dpkg -i succeeds and the || apt-get install -f fallback never fires. Several of the missing libraries are dlopened (libnss3's libssl3.so, libcairo.so.2), so ldd reports nothing and the failure surfaces only as error while loading shared libraries the first time a document is converted. It appeared to work in testing only because Playwright's install-deps pulled the same libraries in at a later step — LibreOffice was silently depending on Playwright.

Fixed by taking Debian's own libreoffice-core dependencies as the authoritative list rather than hand-maintaining a guess (my hand-written list was wrong twice). Verified on a clean base stage with no Playwright present: probe.txt → probe.pdf.

The build gate was checking the wrong thing. It was command -v soffice — presence, which passes happily on a binary that cannot load. It now converts a document and fails the build if no PDF appears. That check is what caught the second missing library, and it is the part of this PR most likely to save someone later.

Skills unzipped one directory too deep, because the zips carry their own top-level <skill>/ dir. Discovery survived it — the loader globs **/SKILL.md recursively and all four skills declare name: in frontmatter — but a skill without a frontmatter name would silently fail to load, since the filename fallback only applies to a SKILL.md directly in the configured directory. Now staged and moved as the setup script does, and the check is for SKILL.md at the skill root rather than for the directory existing.

Also

The bundle is bind-mounted rather than COPYed: committing 1.2 GB to a layer the install step then cannot delete is pure waste. stageOfficeBundle now stages the whole bundle (minus macOS artifacts, useless in a Linux guest) since the installer needs the artifacts themselves, not just the arch-independent zips.

Verification

Offline NuGet restore — the component this PR adds — verified end to end in a sealed container (--network none, arm64 guest image):

=== skill layout ===
/opt/codev-skills/minimax-docx/SKILL.md          <- at the root, nesting fixed
=== nuget config seeded into home ===
    <clear />                                     <- nuget.org genuinely unreachable
    <add key="offline" value="/opt/codev-office/nuget-offline-feed" />
=== restore (NO NETWORK) ===
  Restored MiniMaxAIDocx.Cli.csproj (in 622 ms).
  Restored MiniMaxAIDocx.Core.csproj (in 622 ms).
=== build ===
  0 Error(s)
=== run: create a real docx ===
  Created report document: /tmp/offline.docx      (2009 bytes)

With <clear/> the restore had nowhere to go but the offline feed, so this proves the feed genuinely substitutes for nuget.org. This is precisely what failed before: the feed was not in the image and a sandbox cannot reach nuget.org, so the docx skill was unusable in every sandbox the old pipeline built.

Also verified:

  • LibreOffice conversion gate passes inside the real build (arm64, 417s): probe.txt → probe.pdf via writer_pdf_Export, no missing-library error — the defect that broke the two previous build attempts.
  • Bundle versions present in the built image: pandoc 3.10.1, .NET SDK 8.0.423, LibreOffice 26.2.5 debs, all 4 skills, both node-module trees, chromium + headless shell + ffmpeg, and the 6-package NuGet feed.
  • Python set matches the bundle on both arches, magika 0.6.2 included.

A full two-arch build is running at the time of writing; sizes and hashes to follow. Expect a substantially larger download — the full TDF LibreOffice suite, the .NET SDK and Chromium replace a trimmed Debian subset — so the changelog entry tells users to expect that and to hit Reset to pick the new image up.

packages/codev-sandbox is fork-only, so no DIVERGENCES rows apply.

🤖 Generated with Claude Code

…ersions

install-office.sh was a loose re-interpretation of codev-office-ubuntu-setup.sh
rather than a translation of it. It apt-installed pandoc and LibreOffice, took
whatever the dotnet 8.0 channel currently served, and pip-installed the Python
set from PyPI — so the guest ran pandoc 2.17.1.1 against the bundle's 3.10.1,
LibreOffice 7.4.7.2 against 26.2.5, SDK 8.0.424 against 8.0.423, and unpinned
Python packages. It also ignored two bundle components outright: the offline
NuGet feed, which the docx skill's CLI restores DocumentFormat.OpenXml from and
which a sealed guest cannot reach nuget.org to replace, and Playwright's
Chromium, without which the pdf skill has no browser to render covers with.

Versions now come off the bundle's own filenames, so refreshing the bundle
moves the image with it. The bundle's x86_64 binaries are used verbatim on
amd64; arm64 fetches the same version arch-correct (LibreOffice's aarch64 deb
set, pandoc's arm64 deb, dotnet-install --version, and PyPI under a constraints
file generated from the bundle's own cp311 wheels). A version the bundle pins
that cannot be matched on an arch is a hard failure, never a substitution.

Two defects found by verifying the built image rather than the script:

LibreOffice was installed but could not execute. The TDF debs declare almost no
dependencies, so `dpkg -i` succeeds and the `-f` fallback never fires, leaving
system libraries absent — and LibreOffice dlopens several of them (libnss3's
libssl3.so, libcairo.so.2), so they are invisible to ldd and surface only as
"error while loading shared libraries" at first use. It appeared to work only
because Playwright's install-deps happened to pull the same libraries in later.
Debian packages the same software, so its libreoffice-core dependencies are now
taken as the authoritative list. The build additionally converts a document and
fails if no PDF comes out: the previous gate was `command -v soffice`, which
passes happily on a binary that cannot load.

Skills unzipped a directory too deep, because the zips carry their own
top-level <skill>/ dir. Discovery survived it (its glob is recursive and these
skills name themselves in frontmatter), but a skill without a frontmatter name
would silently fail to load. Staged and moved as the setup script does, and the
check is now for SKILL.md at the skill root rather than the directory existing.

The bundle is bind-mounted rather than COPYed, since committing 1.2 GB to a
layer that the install step then cannot delete is pure waste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 6caef91 into main Aug 16, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the fix/sandbox-office-image branch August 16, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant