Skip to content

ci(agent-image): eval-only pre-merge check for the compass-agent image - #204

Merged
mattwilkinsonn merged 1 commit into
mainfrom
compass-runner-1749-agent-image-eval-check
Aug 7, 2026
Merged

ci(agent-image): eval-only pre-merge check for the compass-agent image#204
mattwilkinsonn merged 1 commit into
mainfrom
compass-runner-1749-agent-image-eval-check

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

agent-image/ is deliberately not a moon project, so the CI gate never
builds it — the image has zero pre-merge coverage, and an eval-time
break (a bun-pin drift against the toolchain.nix assert) surfaces only
post-merge in the publish workflow while a consumer waits on a tag. This
adds the cheap tripwire that pulls that eval-time class to PR time.

Closes SEA-1749.

How

The check forces the image derivation's .drvPath:

devenv eval containers.agent.derivation.drvPath

This evaluates the derivation — the .prototools read and the bun-drift
assert (agent-image/toolchain.nix:50-56) — and instantiates the .drv,
but realises none of the heavy closure. So it catches an eval-time break
(a bun-pin drift) in seconds and deliberately does not catch a
realise-time break (an entrypoint.nix FOD-hash invalidation), which only
the post-merge publish build exercises. That realise-time class stays
owned by the publish workflow's main-branch failure notification,
unchanged. This is the accepted tradeoff the frozen record names: a cheap
pre-merge tripwire, not a full image build on the hot path.

Placement

A separate PR-triggered workflow, contents:read only (an eval needs no
registry credentials), path-filtered to the image's evaluation inputs:

  • Not a step in ci.yml — folding a non-moon build into the gate's YAML is
    the exact second-source-of-truth staleness the ONE-JOB doctrine forbids.
  • Not a step in publish-agent-image.yml — that workflow deliberately
    carries no PR trigger, to keep every token/secret off fork PRs. This
    check needs a PR trigger and no credentials, so it gets its own
    least-privilege workflow.

Its paths filter is the deliberate inverse of the publish workflow's on
one glob: it includes .prototools (which publish excludes), because a
bun-pin move there can only fail the toolchain.nix assert, never silently
change the output — and failing that assert at PR time is precisely this
check's purpose.

The durable operational shape is folded into
docs/architecture/build-and-ci.md beside the publish section.

Verification

End-to-end, at the exact CI-faithful command (clean env, no direnv/proto,
as on a GitHub-hosted runner):

  • Clean .prototools -> exit 0, emits the .drv path.
  • Drifted .prototools (bun pinned to a bogus version) -> exit 1, with the
    toolchain.nix "has drifted from the .prototools pin" assert message.

Workflow validated with actionlint (0 findings); docs pass
root:markdownlint (0 errors). moon ci green at the pushed head via the
jj-hp gate.

Open Questions

  • [Design fork — parked for Matt, overnight] Placement + required-vs-
    advisory. Implemented as a separate PR-triggered least-privilege
    workflow, advisory (not a required check) — the stated assumption. It
    matches the frozen record's own "separate workflow" reasoning for the
    publish lane, and a nix-eval flake should never red a required merge
    gate. The frozen record specifies the what (eval-only, nix eval, no
    realise) but not the placement, so this is a genuine fork. If required
    (branch-protection config) or a different placement is preferred, say
    so and I'll adjust — not blocking, the check is correct and green either
    way.

Co-authored-by: Matt Wilkinson matt@sealedsecurity.com

@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

SEA-1749

@rigel-mintaka
rigel-mintaka force-pushed the compass-runner-1749-agent-image-eval-check branch 2 times, most recently from a48d670 to 71e6ee5 Compare August 7, 2026 05:12
…on project

agent-image/ was outside moon, so the CI gate never built the image —
it had zero pre-merge coverage, and an image-build break (a bun-drift
assert in toolchain.nix, a stale FOD hash or a broken bundle in
entrypoint.nix) surfaced only post-merge in the publish workflow, while
a consumer waited on a tag. This registers agent-image/ as the
compass-agent-image moon project so the gate builds the image on any PR
that affects its closure, pulling the whole image-build break class to
PR time.
Closes SEA-1749.
## How
agent-image/moon.yml defines a nix `build` task that realises the image
with the same fork-pinned derivation the publish lane ships and the
local dogfood load produces:
    nix run path:../forks/devenv#devenv -- container build agent
so a green build proves the exact artifact that publishes still builds —
both an eval-time break (the bun-pin drift assert) and a realise-time
break (FOD-hash invalidation, a broken bundle), the full class. The `ci`
task depends on `build`; both carry cache:false (nix owns its store-path
caching, so moon must not false-green a build it did not re-run) and
runInCI:true, matching the two vendored fork projects' nix builds.
Registered in .moon/workspace.yml as compass-agent-image, which is what
puts it in the gate: moon discovers projects from that map, so ci.yml's
one-job `moon ci :ci` picks it up with no workflow edit.
## Cost, and why it is bounded
The image closure is the dominant CI cost (the reason ci.yml's timeout is
90m), but it is not paid on every PR. `moon ci` runs a PR's affected
projects only, and the build task's inputs scope it to the image closure:
the agent-image/ tree, packages/compass-agent/, both vendored forks, the
root package.json and bun.lock, and .prototools. A PR touching none of
those never builds the image; every push to main runs it in the full
sweep. The inputs MIRROR publish-agent-image.yml's on.push.paths — the
reviewed source of truth for what changes the published artifact — plus
.prototools (which publish excludes because a pin move there cannot change
the output, but which reddens the toolchain assert, exactly what this gate
catches at PR time). As a project in the one-job gate the image build is
now a required check: a build break blocks merge, the same posture as the
vendored forks.
## A benign hasher warn
moon's task hasher logs "Attempted to hash input
forks/devenv/tests/keycloak-certs/realms but only files can be hashed" —
the vendored devenv fork ships a directory symlink at that path (a
keycloak test fixture, never in the image closure). It is cosmetic and
cannot skew the gate: affected-scheduling is VCS-touched-file based (a
change to or repoint of any closure path still runs the build), and the
hasher's cache key, the only thing the unhashable node feeds, is unused
because the task is cache:false. Documented in the task's inputs comment.
## Verification
- `moon run compass-agent-image:build` realises the image green
  (deterministic output /nix/store/...-image-compass-agent.json).
- moon resolves the build task's inputs to exactly the closure globs
  above (plus moon's own .moon/*.{yml,...} config auto-input).
- Docs pass root:markdownlint (0 errors).
## Supersedes
An earlier eval-only variant of this check (a separate advisory
PR-triggered workflow running `nix eval` the image drv without realising
it) was superseded by the decision to build the full image in the gate:
eval-only caught only the eval-time bun-drift assert, whereas the moon
build also catches realise-time breaks and reuses the nix-in-gate posture
the vendored forks already establish. The frozen record's deferred-
hardening note (docs/designs/platform/compass-agent-image-publish.md) and
the publish workflow's failure-ownership comment are updated to match; the
durable operational shape is folded into docs/architecture/build-and-ci.md.
Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
@rigel-mintaka
rigel-mintaka force-pushed the compass-runner-1749-agent-image-eval-check branch from 71e6ee5 to 9e71c4c Compare August 7, 2026 15:58
@mattwilkinsonn
mattwilkinsonn merged commit bf7dbf6 into main Aug 7, 2026
1 check passed
@mattwilkinsonn
mattwilkinsonn deleted the compass-runner-1749-agent-image-eval-check branch August 7, 2026 16:35
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.

2 participants