Skip to content

feat(isolation): add generic devenv sandbox image built and published to GHCR - #395

Merged
basiliskus merged 5 commits into
mainfrom
feat/devenv-image
Aug 28, 2026
Merged

feat(isolation): add generic devenv sandbox image built and published to GHCR#395
basiliskus merged 5 commits into
mainfrom
feat/devenv-image

Conversation

@basiliskus

Copy link
Copy Markdown
Contributor

Summary

Adds a generic devenv sandbox base image under integrations/isolation/images/devenv/ — Nix (single-user) + devenv + direnv on a parametrized BASE_IMAGE — plus a CI workflow that builds a multi-arch (amd64 + arm64) manifest for two variants (devenv-shell, devenv-opencode) and publishes them to GHCR. Teams consume it with ACQ_IMAGE=ghcr.io/gsa-tts/agentic-coding-patterns/devenv-{shell,opencode}:<tag> on either backend, replacing the load-a-4.7-GB-tar-per-machine flow.

Eligibility & Accountability

  • I am a federal employee or contractor (PRs merge with @GSA-TTS/agentic-coding-team code-owner approval; eligibility is attested here, not authenticated). Anyone may open issues without this.
  • I understand this change and can explain it in my own words — I am the author of record and accountable for it.
  • By opening this PR, I attest I have the right to release this work under the repository's CC0-1.0 dedication.

AI Assistance Disclosure

  • AI tools were used in this contribution. If checked, note which and for what: Claude Code — generalized the Dockerfile from our team template, authored the GHCR workflow and README.
  • I verified all AI-generated content: no fabricated/hallucinated APIs, dependencies, citations, or placeholder content; any AI-suggested dependency was confirmed to exist.
  • Any inherited/reused third-party material retains its original license and is identified (not represented as public domain).

Type of Change

  • New tool/editor integration (integrations/)
  • Infrastructure/tooling change

Motivation

Repos using devenv need Nix + devenv + direnv baked into the sandbox image — create-time installs are impractical (multi-GB Nix closure; egress policies rightly block package mirrors). login.gov Team Data has run this template in production since June, but distribution was per-machine tar loads. Nothing in the image is team-specific, so it belongs here as a published, generic artifact kits layer on. Design per #394: no baked proxy CA (runtime trust stays the zscaler-ca-certificate kit's job; EXTRA_CA_CERT build arg keeps behind-proxy local rebuilds possible), minimal apt payload (with Nix baked, kits and devenv.nix deliver tools), team config stays in kits.

Testing

  • Ran make validate locally (all checks pass)
  • Tested with at least one real scenario — built the devenv-shell variant locally (native arm64, behind Zscaler via EXTRA_CA_CERT); smoke test as the agent user resolves devenv 1.11.2, direnv 2.36.0, nix 2.24.9 from the baked .bashrc wiring.
  • Commit messages follow Conventional Commits format

Safety Checklist

  • No secrets, API keys, tokens, or passwords included
  • No PII (names, emails, SSNs, addresses)
  • No CUI (Controlled Unclassified Information)
  • No internal URLs, hostnames, or system paths
  • No customer data or proprietary information

Validation Results

make validate
✓ All validators passed

Related Issues

Closes #394

Additional Context

  • The workflow only runs post-merge (path-filtered pushes to main, plus workflow_dispatch), so the first GHCR publish happens after merge; the new packages will need visibility flipped to public once created.
  • amd64 is exercised for the first time in CI (local validation was native arm64).
  • End-to-end validation against our team kit (sbx verify suite + msb path) will follow the first published tag.

… to GHCR

Generalized from login.gov Team Data's production template (per #394):
Nix (single-user) + devenv + direnv on a parametrized BASE_IMAGE, with
shell and opencode variants. No baked proxy CA — runtime trust stays a
kit's job; an EXTRA_CA_CERT build arg keeps behind-proxy local rebuilds
possible. Minimal apt payload (xz-utils, ca-certificates): with Nix
baked, kits and devenv.nix deliver tools at create/run time.

CI builds a multi-arch manifest (amd64 + arm64 on native runners,
push-by-digest + merge, registry layer cache) and pushes to GHCR on
path-filtered pushes to main plus manual dispatch. Consumed on both
backends via ACQ_IMAGE=ghcr.io/gsa-tts/agentic-coding-patterns/
devenv-{shell,opencode}:<tag>.

Closes #394
Path-filtered pull_request trigger runs the same 2x2 matrix without
publishing: no GHCR login, no digest push, no cache write (fork PRs
lack packages:write, and PR runs must not write the shared layer
cache). Each job loads its single-arch image and smoke-tests
devenv/direnv/nix as the agent user, so amd64 and the arm runner path
are exercised before merge; the merge job stays main-only.
…PATH

Self-review findings: put the Nix profile on ENV PATH so the toolchain
is visible to non-interactive execs (bash -c never reads .bashrc);
sha256-pin the Nix installer script (release assets are mutable);
narrow the publish trigger to Dockerfile/VERSION so docs-only changes
can't repoint published tags, and make the merge job refuse to reuse an
already-published version tag; gate publishing on the main ref so a
branch workflow_dispatch builds without touching production tags.
@basiliskus
basiliskus marked this pull request as ready for review August 27, 2026 19:40
@basiliskus
basiliskus requested a review from a team as a code owner August 27, 2026 19:40

@wz-gsa wz-gsa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — LGTM, supply chain independently verified end-to-end

This is exactly the kind of PR worth checking every pin on, not just reading the Dockerfile for shape. I verified every integrity claim live rather than trusting the comments.

Supply-chain pins — all verified against the real sources:

  • Nix installer SHA-256: fetched the actual release asset (nix-installer.sh v0.27.0) and hashed it — matches NIX_INSTALLER_SHA256 exactly.
  • nixpkgs rev (ac62194c...): resolves to a real commit on the release-25.05 backport branch, not a dangling/garbage SHA.
  • All 6 pinned GitHub Actions (checkout, setup-buildx-action, login-action, build-push-action, upload-artifact, download-artifact): dereferenced every tag through the GitHub API — every full-length commit SHA in the workflow matches its stated tag exactly.
  • zizmor: 0 medium/high findings; the 2 --pedantic-only low findings (missing comments adjacent to packages: write) are style, not substance — the actual security reasoning for that scope is present elsewhere in the workflow.

Reviewed the two things that looked riskiest on first read and both check out:

  1. apt-get update || true — the fail-open is correctly scoped to just the update step. The &&-chain still propagates a genuine apt-get install failure (e.g. if the failed update left no cached xz-utils), so a real installability problem still aborts the build loudly; only the proxy-blocked-update case is absorbed, matching the comment's justification.
  2. Multi-user installer + same-layer chown -R agent:agent /nix — this is the standard container workaround for Nix's installer (no clean single-user-from-scratch path in this installer generation), and doing the chown in the same RUN as the install (not a separate layer) correctly avoids doubling the image size with a redundant /nix layer.

Version-immutability guard is sound. docker buildx imagetools inspect before push, refusing to republish an existing version tag; concurrency: group: devenv-image-${{ github.ref }} with cancel-in-progress: false serializes runs on the same ref rather than racing them, so a concurrent second push/dispatch queues instead of creating a TOCTOU window between the inspect-check and the manifest push.

Scope discipline matches the README's own framing — confirmed this is deliberately NOT registered in kits.yaml (it's a base image, not a kit), and the Dockerfile bakes only the base-agnostic toolchain (Nix/devenv/direnv), correctly pushing runtime CA trust, team config, and store sizing to kits that layer on top — consistent with the "delegated to kits" design Basilio described.

Nice design detail worth calling out: EXTRA_CA_CERT defaults empty for CI builds (a public generic image should never bake a proxy CA) and is documented as a local-rebuild-only escape hatch — the right default for something published to a public registry.

All 8 CI checks green. Approve.

AI-assisted (OpenCode).

@basiliskus
basiliskus merged commit 8c7e9ca into main Aug 28, 2026
8 checks passed
@basiliskus
basiliskus deleted the feat/devenv-image branch August 28, 2026 17:13
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.

Proposal: a generic Nix/devenv agent template in patterns, built and published to GHCR by CI

2 participants