Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ result-*

# NOTE: generated clients (go/gen, packages/compass-client/src/gen)
# are intentionally committed and CI drift-gated — do not ignore them.

# runner-image build outputs: the staged nix closure (which carries the
# entrypoint symlink) and the OCI layout, realised by tools/runner-image/build.ts.
/runner-image/store/
/runner-image/out/
11 changes: 11 additions & 0 deletions .moon/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,23 @@ projects:
# cache lane or V2a boot bring-up. Same affected-detection posture as
# compass-agent-image (see guest-image/moon.yml).
compass-guest-image: 'guest-image'
# The Runner container image: the compass-runner binary, the KVM userland and
# the guest assets on a minimal hardened base. Its `build` is runInCI:false (it
# needs a buildkitd and a multi-GB realise), so registration here is for graph
# membership and affected-detection, not a gate leg; the gate coverage is the
# runner-image tool's pure-core suite below. See runner-image/moon.yml.
compass-runner-image: 'runner-image'
# The local microVM boot-test lane (RIG-2591): realises the guest image + VMM
# stack from nix and execs the KVM-gated `go test -tags microvm` suite the
# untagged compass-go:test lane never builds. Registered so its typecheck +
# unit tests ride the moon-driven CI sweep; the boot lane itself lives on
# compass-go:test-microvm (runInCI:false — it needs KVM + a nix build).
microvm-boot-test: 'tools/microvm-boot-test'
# The runner-image build lane: realises the nix closure, stages it, and drives
# the Dockerfile build. Registered so its typecheck + pure-core unit tests ride
# the CI sweep — that suite is what catches a build-arg or closure-root drift,
# since no compass CI step can build the image itself today.
runner-image: 'tools/runner-image'
# The Compass native-app release bundle: a heavy nix build (realises the
# WebKitGTK cc/pkg-config closure) that stages the versioned tarball. Same
# affected-detection posture as compass-agent-image — registered here so the
Expand Down
12 changes: 12 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions runner-image/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Context prune for the runner image build. The context is runner-image/ itself,
# which holds only the Dockerfile, the staged store/ closure and the
# compass-runner entrypoint symlink — so this excludes the build OUTPUT (out/),
# which would otherwise be re-transferred into every rebuild's context and grow
# it without bound.
out
101 changes: 101 additions & 0 deletions runner-image/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# syntax=docker/dockerfile:1.7-labs
# The compass-runner container image (R1 of the Compass Runner containerization
# record). Carries the Runner binary, the KVM userland it exec's
# (cloud-hypervisor, virtiofsd, passt) and the three guest assets the microVM
# backend direct-boots, so a Runner pod needs no host toolchain — only /dev/kvm
# and the session-volume mount.
#
# A Dockerfile on a minimal base, not a nix2container image: the mechanism
# follows the image's RUNTIME, and the Runner is a prebuilt application that
# runs no toolchain. Full rationale in the design record's R1 task; nix still
# builds every carried artifact, and build.ts COPYs the result.

# Distroless STATIC, not :base and not Alpine. The carried binaries are
# dynamically linked but nix-closed: each names an ABSOLUTE /nix/store
# interpreter and resolves every NEEDED library through its own RPATH, so the
# closure supplies its own ld.so and glibc and the base is never consulted for a
# library. That makes the base's own libc dead weight, and `static` is the
# smallest base that still provides what IS needed from it: /etc/passwd +
# /etc/group (the nonroot uid), CA certificates (the Runner dials the Server
# over TLS), and /tmp. Verified: with the closure present all four binaries
# execute; with ONLY cloud-hypervisor's own store path copied — its glibc
# omitted — the same exec fails "missing dynamic library", which is the control
# proving the loader comes from the closure rather than the base.
#
# The pinned digest is the nonroot variant (uid/gid 65532). The Runner needs no
# root: /dev/kvm access is granted by supplementary group at the pod layer.
# Pinned by digest, never by tag — GHCR-style tag mutability gives no
# immutability guarantee.
FROM gcr.io/distroless/static-debian12@sha256:c0f429e16b13e583da7e5a6ec20dd656d325d88e6819cafe0adb0828976529dc

# The realised nix closure: the Runner binary, the KVM userland and the guest
# assets, each at its own absolute /nix/store path. ONE COPY of a
# build.ts-staged directory rather than a COPY per component, because the store
# paths reference each other by absolute path — splitting them across layers by
# component would interleave a binary and its glibc into different layers for no
# cache benefit, since a Go rebuild moves the Runner path and nothing else.
#
# Ownership is left at the default (root-owned, world-readable) and the image
# runs as nonroot: the closure is read-only at runtime, so a non-root process
# needs no write access to it. This also keeps the layer bit-identical to the
# staged tree, which is what makes the digest reproducible across rebuilds.
COPY store /nix/store

# The stable entrypoint name is /nix/store/.compass-runner, a RELATIVE symlink
# build.ts stages inside store/ (so it arrives with the COPY above — there is no
# second COPY). ENTRYPOINT is exec-form and cannot expand a build ARG, while the
# Runner's own store path carries a hash that moves on every Go rebuild; the
# symlink gives the image a fixed entrypoint whose target moves with the build.
#
# It must be relative and inside the staged tree. An absolute symlink at the
# context root dangles on the BUILD HOST, and BuildKit checksums context entries
# before any COPY runs, so such a link fails the build outright with "not found"
# rather than resolving later inside the image.

# Absolute store paths, resolved at build time from the staged closure by
# build.ts and passed in as build args. They are baked into ENV rather than
# hardcoded because every path carries a content hash that moves on any rebuild
# of its input.
#
# The Runner reads each of these as the documented env fallback for the matching
# --microvm-* flag (main.go registerBackendFlags), so an operator can still
# override any one on the command line without rebuilding the image. Setting
# them here is what makes the image self-describing: the microVM backend's
# preflight is fail-closed (it requires both a static support probe and a real
# boot canary), so a missing or wrong path fails at startup, loudly, rather than
# at first session.
ARG VMM_BIN
ARG VIRTIOFSD_BIN
ARG GUEST_KERNEL
ARG GUEST_ROOTFS
ARG GUEST_INITRD

ENV COMPASS_RUNTIME_BACKEND=microvm \
COMPASS_MICROVM_VMM=${VMM_BIN} \
COMPASS_MICROVM_VIRTIOFSD=${VIRTIOFSD_BIN} \
COMPASS_MICROVM_KERNEL=${GUEST_KERNEL} \
COMPASS_MICROVM_ROOTFS=${GUEST_ROOTFS} \
COMPASS_MICROVM_INITRD=${GUEST_INITRD}

# passt is exec'd by name, not by configured path (unlike the VMM and virtiofsd,
# which have --microvm-* flags), so its bin dir must be on PATH. Prepended to
# the base PATH rather than replacing it.
# Consumed by the dockerfile frontend (paired with the export's
# rewrite-timestamp) to fix the config `created` field and the layer mtimes, so
# two builds of the same inputs yield the same digest.
ARG SOURCE_DATE_EPOCH

ARG STACK_BIN_DIR
ENV PATH=${STACK_BIN_DIR}:/usr/local/bin:/usr/bin:/bin

# ENTRYPOINT, not CMD: the image runs exactly one program, and an ENTRYPOINT
# lets a pod spec's `args` add flags without re-stating the binary path. Exec
# form, so the Runner is pid 1 and receives SIGTERM directly at pod shutdown —
# a shell wrapper would swallow it and leave the graceful drain to the kill
# timeout.
# Explicit, rather than inherited from the base's own config: a future digest
# bump that landed on the root `static` variant would otherwise silently produce
# a root-running container, and nothing here would catch it.
USER 65532:65532

ENTRYPOINT ["/nix/store/.compass-runner"]
72 changes: 72 additions & 0 deletions runner-image/moon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# yaml-language-server: $schema=https://moonrepo.dev/schemas/project.json
#
# compass-runner-image (runner-image/): the Runner container image — the
# compass-runner binary, the KVM userland it exec's (cloud-hypervisor,
# virtiofsd, passt) and the three guest assets the microVM backend direct-boots.
# This project carries the Dockerfile and the staged build context; the
# realise-and-stage logic is tools/runner-image (TypeScript, not a shell script).
#
# A Dockerfile project, not a nix one like the sibling agent-image/ and
# guest-image/: their artifacts ARE nix derivations, this image's runtime is a
# distroless base. See the design record's R1 task.
#
# WHY THERE IS NO `ci` TASK. Unlike agent-image/ and guest-image/, this project
# deliberately does NOT ride the pre-merge gate: the build needs a reachable
# buildkitd (which no compass CI step carries today) and it
# stages a multi-GB closure dominated by the guest rootfs. Registering a `ci`
# task now would add an unrunnable task to the gate, so the gate coverage this
# project has today is the pure-core suite in tools/runner-image, which is where
# a mapping drift would actually be caught. The publish lane adds the first CI leg that builds this image.
layer: 'application'
language: 'bash'
# ci-group.nix with the sibling image projects: the CI matrix requires every
# project to carry exactly one group tag, and this project's real build closure
# is the nix one those legs already provision. It contributes no CI task today
# (`build` is runInCI:false), so the tag places it in the graph rather than
# adding a leg.
tags: ['ci-group.nix']

workspace:
inheritedTasks:
# Not a bun package — no package.json, no bun.lock — so the tag-bun `install`
# must never run here; `lint` and `format` are whole-repo tasks on the root
# project, never inherited. The same defensive guard the sibling image
# projects carry.
exclude: ['install', 'lint', 'format']

tasks:
build:
# Realise the closure and build the image. runInCI:false — the build needs a
# buildkitd and a multi-GB nix realise, so this is the dev-box/publish entry
# point, not a gate task. cache:false — nix owns the store-path caching and
# buildkit owns the layer caching, so moon must not false-green a build it
# did not re-run (the posture the sibling image projects take).
command: 'bun tools/runner-image/build.ts'
options:
cache: false
runInCI: false
runFromWorkspaceRoot: true
# The image's true build closure. A leading `/` is workspace-root-relative
# (the convention the sibling image projects use); the bare glob is this
# project's own tree. Each entry is an input whose change alters the built
# image: the Dockerfile and the build script directly, the Go module +
# runner command for the binary, and the guest-image/VMM-env closures for
# the carried assets. A glob that misses one would leave a stale image
# passing as current.
# Named tracked files, NOT a `**/*` glob. moon hashes with its native walker,
# which reads contents directly and does not skip gitignored paths — a bare
# glob here would hash the multi-GB staged closure this task itself writes
# into store/, on every affected-computation.
inputs:
- 'Dockerfile'
- '.dockerignore'
- 'moon.yml'
- '/tools/runner-image/**/*'
- '/flake.nix'
- '/flake.lock'
- '/go/go.mod'
- '/go/go.sum'
- '/go/cmd/compass-runner/**'
- '/go/internal/**'
- '/guest-image/**'
- '/tools/toolchain/microvm-vmm-env.nix'
4 changes: 4 additions & 0 deletions tools/runner-image/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "//",
"linter": { "rules": { "suspicious": { "noConsole": "off" } } }
}
Loading
Loading