Publish the compass-agent image to GHCR
Design record. This designs the GHCR publish lane for the
compass-agentruntime image; it targets thesealedsecurity/compassrepo — everyagent-image/*,.github/workflows/*,forks/*,go/cmd/*,ci.yml,devenv.nix,packages/compass-agent/*, anddocs/architecture/*citation below is a path in that repo at HEADb3fc25311, not this one (line numbers drift as the code evolves; resolve them against that commit). It lives in the sealed design corpus (docs/designs/platform/) because that is where the wave’s design records freeze; thedocs/designs/product/*cross-references (e.g. DL-112) are paths in this (sealed) corpus.
Status: Draft Tracking: SEA-1690 (blocks compass-native SEA-1683/T2, SEA-1685/T4, SEA-1687/T6)
Problem / Intent
Section titled “Problem / Intent”The compass-agent base image already builds and loads locally
(dogfood:agent-image → containers-storage:compass-agent:latest), but per
frozen decision DL-112 (docs/designs/product/compass-native-app/design.md
§OQ6) the native app does not bundle the agent image: compass-stack podman pulls it from GHCR at first run. Nothing publishes the image today. This
record designs the publish lane — the GHCR side of that seam; the pull side is
compass-native’s (SEA-1683/T2).
Approach
Section titled “Approach”Decision: the name/tag contract (confirmed with compass-native)
Section titled “Decision: the name/tag contract (confirmed with compass-native)”Settled peer-to-peer with compass-native (the consumer) — recorded as a decision, not an assumption:
- Ref:
ghcr.io/sealedsecurity/compass-agent— locked. - Tags, both published per main build:
:git-<short-sha>(12-hex,git rev-parse --short=12 HEAD) — immutable. This is the pin compass-stack bakes into the native app binary and hands the runner via--image/$COMPASS_AGENT_IMAGE(go/cmd/compass-runner/main.go:44-45— “The container image every agent workstream runs. Defaults to $COMPASS_AGENT_IMAGE.”;:111-114requires it). The publish step refuses to overwrite an existing:git-<sha>tag whose content differs (a config-digest compare, exact commands in T1), and re-inspects after every copy to assert the pushed digest — so immutability is enforced and verified, not assumed.:latest— moving; documented first-run fallback only, never the default consumption path.
- Forward note (non-load-bearing): GA (SEA-1687) will likely add a
release-version tag (e.g.
:v1.2.3) alongside:git-<sha>. The publish script below takes its tag list as arguments, so that is a one-line later add — no scheme change. - Visibility: the package is public (decided — see the visibility Decision below). The lane works either way; public means no pull credential anywhere.
Decision: platform contract — linux/amd64 single-arch (dogfood milestone)
Section titled “Decision: platform contract — linux/amd64 single-arch (dogfood milestone)”A settled fact, not an open question: the consumer’s arch is frozen in
compass-native’s merged record (PR #1073,
docs/designs/product/compass-native-app/design.md) to Linux x86_64 for the
dogfood milestone — non-Linux runner support and
macOS packaging are deferred there to a GA follow-up (:522-524 “reproducible
build of the app bundle for Linux (the dev/dogfood target; macOS packaging
tracked as follow-up per A5)”; :246-248 “non-Linux runner support … deferred
to follow-up”; :346-349 runner is Linux-only with an embedded-mode
preflight-and-refuse on an unsupported host; :486 dogfood gate = “e2e smoke on
the Linux dev box”, which is x86_64). So publish and consumer agree on
linux/amd64, and the single-arch two-skopeo copy mechanism below is correct
by design, not by omission.
- Forward add (non-load-bearing), tied to GA native-packaging (compass-native
T5/T6): macOS/
aarch64support is both a native-packaging item and a multi-arch manifest-list image here. The per-platform entrypoint FOD hash (agent-image/entrypoint.nix:96-116) and the manifest-list push (which the two-copy path does not produce — needing per-arch builds on matching runners plus amanifest create/push) enter scope in that record, not this one.
Decision: build+push mechanism — build the nix2container spec once, push each tag with the fork’s skopeo
Section titled “Decision: build+push mechanism — build the nix2container spec once, push each tag with the fork’s skopeo”The image is a devenv/nix2container artifact, not a docker build. The devenv
container module builds a nix2container image spec and copies it with a
patched skopeo that understands the nix: transport
(forks/devenv/src/modules/containers.nix:307):
${nix2container.skopeo-nix2container}/bin/skopeo --insecure-policy copy "nix:$container" "$dest"with dest="${registry}${cfg.name}:${cfg.version}" (containers.nix:295) —
the tag is pinned to cfg.version, which defaults to "latest"
(containers.nix:324-328) and is not overridable from the CLI: devenv container copy exposes only --registry and --copy-args
(forks/devenv/devenv/src/cli.rs:1104-1113). So the stock copy path can push
:latest but can never mint :git-<sha>.
Chosen mechanism:
-
Build the spec exactly as dogfood does:
nix run path:../forks/devenv#devenv -- container build agent, cwdagent-image/.container buildprints the image-spec store path to stdout (forks/devenv/devenv/src/main.rs:910-912emits it asCommandResult::Print, a bareprint!after UI cleanup,main.rs:830-832;container copycalls the samecontainer_buildinternally,forks/devenv/devenv/src/devenv/container.rs:55), so this is byte-for-byte the derivation the dogfood task loads intocontainers-storage:— the reproducibility constraint holds by construction: one derivation, two copy destinations. Capture hygiene: nix/devenv tracing goes to stderr, but the script takes the last stdout line and asserts it matches^/nix/store/before use, so a future fork bump that adds a stdout line cannot silently feed skopeo a non-path. -
Push each tag straight from the spec with the fork’s patched skopeo (
forks/nix2container/flake.nix:31exposesskopeo-nix2container;default.nix:22builds it frompkgs.skopeowith thenix:transport patch — the same pattern nix2container’s owncopyToRegistryhelper uses,default.nix:78-81):skopeo --insecure-policy copy nix:$SPEC docker://ghcr.io/sealedsecurity/compass-agent:git-<sha>skopeo --insecure-policy copy nix:$SPEC docker://ghcr.io/sealedsecurity/compass-agent:latestThe second copy re-uploads nothing: skopeo skips blobs the registry already has, so
:latestcosts one manifest write.
Alternatives weighed:
- (a)
devenv container copy agent --registry docker://ghcr.io/sealedsecurity/then retag — reuses the highest-level CLI, but can only produce:latest(tag fixed atcfg.version), forcing a registry→registry retag copy for the immutable tag and pushing the moving tag first — the wrong order (the pin should exist before:latestmoves). Rejected: more moving parts for less control. - (b) wire nix2container’s
copyToRegistryas a nix attr — the devenv module does not expose the raw image derivation as a build target (derivationis aninternal = trueoption,containers.nix:523-527), so this means new plumbing through the vendored fork for something two skopeo invocations do. Rejected: fork surface area for no gain. - (c)
podman pushfromcontainers-storage:after a dogfood-style load — round-trips through the local store and re-computes layers on push; loses the direct spec→registry path and adds a store dependency to CI. Rejected.
Decision: CI placement — a separate least-privilege workflow, a principled exception to ONE-JOB
Section titled “Decision: CI placement — a separate least-privilege workflow, a principled exception to ONE-JOB”New workflow .github/workflows/publish-agent-image.yml, not a step in the
existing CI job.
The ONE-JOB doctrine (.github/workflows/ci.yml:4-23) exists to prevent a
second source of truth for what the gate covers: “it can only be built by
enumerating projects (or tasks) in YAML, and a project list in a workflow is a
second source of truth for something .moon/workspace.yml already owns.” The
publish lane is not the gate and enumerates no moon projects — agent-image/
is deliberately not a moon project (.moon/workspace.yml has no entry for it;
it is a standalone devenv) — so a separate workflow re-creates none of the
silent-staleness failure the doctrine targets. What it does buy:
- Least privilege. A GHCR push needs
packages: write; the gate job runscontents: read(ci.yml:76-77, workflow-level). GitHub permissions can be scoped per-job, so a second job inci.ymlwith its ownpackages: writewould not widen the gate job’s token — but a separate workflow is the simpler boundary: it getscontents: read+packages: writeand nothing else, and PR events never reach it at all. - Paths-scoping.
ci.yml’son:has no per-job path filter; scoping publish to closure-affecting pushes insideci.ymlwould need adorny/paths-filterstep, where a separate workflow gets nativeon.push.paths(next section). - Concurrency. The gate’s concurrency group cancels in-progress runs
(
ci.yml:74-76,cancel-in-progress: true) — a superseding push would kill an in-flight publish mid-tag-pair. The publish needs its owncancel-in-progress: falsegroup, which a separate workflow gives cleanly. - Off the hot path, not a required check. The image closure is the heavy
nix build that motivates CI’s 90m timeout (
ci.yml:93-95); the dogfood task is opt-in for the same reason (devenv.nix:340-348). A separate workflow keeps PR latency untouched, and a publish flake never reds the required merge gate. - Failure ownership.
agent-image/is not a moon project, so the gate never builds it — the image has zero pre-merge coverage, and the publish is not required. A red publish (a.prototools/nixpkgs bun-drift assert,agent-image/toolchain.nix:50-56; an FOD-hash invalidation,entrypoint.nix:116) would otherwise sit unnoticed while a consumer waits on a tag. Publish failures are owned by compass-runner (this record’s owner); the main-branch Actions failure notification is the surfacing mechanism for a push failure. The image BUILD is now gated pre-merge by thecompass-agent-imagemoon project (the deferred-hardening item below, implemented), so a build break is caught on the PR rather than here.
Triggers: push: branches: [main] with a paths: filter (next section), plus
workflow_dispatch as the manual/backstop lane. No PR trigger — no secret or
token exposure to fork PRs, no per-PR heavy build. No tag/release trigger yet;
that arrives with the GA release-version tag (forward note above).
Alternative weighed: fold into the CI job as a main-only conditional step.
Keeps one workflow file, but widens the gate token (above), couples publish
latency/failures to the required check, and buys nothing — the publish shares
no setup with the gate beyond nix installation. Rejected.
Decision: publish only when the image closure changes, with a manual backstop
Section titled “Decision: publish only when the image closure changes, with a manual backstop”paths: filter on the workflow’s push trigger, approximating the image’s
nix closure:
agent-image/**packages/compass-agent/**forks/devenv/**forks/nix2container/**package.jsonbun.lock.github/workflows/publish-agent-image.yml(agent-image/entrypoint.nix bundles packages/compass-agent and pins the
whole workspace dependency tree to the root bun.lock via a fixed-output
derivation whose src reads every workspace-member manifest —
(builtins.fromJSON (readFile ../package.json)).workspaces.packages expanded
with readDir over packages/*/apps/*/tools/*, entrypoint.nix:48-65,
realised at :140-148; the build itself runs through forks/devenv and its
nix2container input. .prototools is deliberately excluded — a bun-pin move
there can only fail the toolchain.nix:50-56 assert, never silently change the
output, so it needs no publish trigger. The workflow file itself is in the
filter so a fix to the publish lane republishes.)
- The build is the expensive part (the full agent toolchain closure), not the transfer — GHCR layer dedup makes an identical push cheap, but the nix build that precedes it is exactly the cost the repo keeps off every hot path. Publishing on every main push would pay it on every docs/Go/UI commit for an identical artifact.
- The cost of filtering: a path the filter misses (e.g. a future input outside
these globs) skips a publish, and
:git-<sha>tags exist only for shas where publish ran. The same is true of concurrency supersession — GitHub keeps at most one pending run per group, so a burst of pushes A,B,C publishes A and C and silently skips B’s tag even though its paths matched. Both gaps are acceptable and mean the same thing: a published tag is the source of truth, a missing one is not a failure. compass-stack pins a published sha (it selects the pin at native-app build time from published tags, so gaps are fine), andworkflow_dispatchrepublishes any HEAD on demand. The filter list lives beside the workflow with a comment tying it to the closure inputs, so extending the closure prompts extending the filter in review.
Alternative weighed: publish on every main push (simplest, no filter to go
stale). Rejected for CI spend: the heavy nix build per commit is the exact cost
ci.yml’s affected gating and the opt-in dogfood task exist to avoid.
Decision: push auth — the workflow’s own GITHUB_TOKEN
Section titled “Decision: push auth — the workflow’s own GITHUB_TOKEN”skopeo login ghcr.io -u ${{ github.actor }} --password-stdin fed from
secrets.GITHUB_TOKEN, with permissions: packages: write on the workflow.
Scoped to this repo’s GHCR namespace, auto-rotated, nothing to provision or
leak. Alternative: a PAT/deploy token — needed only for cross-repo or
cross-org pushes, which this is not; a standing secret to rotate for zero
benefit. Rejected.
Auth-file pin (load-bearing across the two nix run invocations): skopeo login and the later skopeo copy run as separate nix run processes and must
resolve the same credentials file. The default location is
$XDG_RUNTIME_DIR/containers/auth.json, whose presence on GitHub-hosted
runners is environment-dependent — a mismatch greens the login step and 401s
the copy. The workflow therefore exports an explicit REGISTRY_AUTH_FILE
(e.g. $RUNNER_TEMP/ghcr-auth.json) that both invocations honor; publish.sh
passes --authfile "$REGISTRY_AUTH_FILE" to every skopeo call as a belt-and-
braces guard.
First-publish caveat (operational, one-time): the first GITHUB_TOKEN push
creates the compass-agent package linked to this repo, private by
default, and only if the org’s package-creation policy permits
GITHUB_TOKEN-created packages (an org setting; if it forbids them, the first
run 403s and an owner must pre-create the package or relax the policy). Whoever
executes the plan must confirm that precondition, set the package public
(per the visibility Decision below) in the package settings once, and confirm
the repo-linkage grants the workflow write access thereafter.
Decision: package visibility — public (Matt’s ruling)
Section titled “Decision: package visibility — public (Matt’s ruling)”The GHCR package is public. Matt ruled it directly: compass is
open-source, so the image’s payload — the toolchain plus the bundled
first-party agent entrypoint (readable, unminified bun build JS of
packages/compass-agent and its import graph) — is source that is public by
the project’s own nature, not a leak. The image carries no runtime secrets
(those are runner-supplied per-exec, agent-image/devenv.nix:50-53). Public is
also what makes DL-112’s no-credential first-run podman pull work with
nothing to provision.
Consequences folded through this record: the pull side needs no credential (the
DL-109 keychain flow is a private-visibility contingency only, kept in the note
below for the record); the one-time package-visibility setting after first push
is “set public”; and an unauthenticated skopeo inspect becomes a meaningful
acceptance signal (a T3 follow-up — it exercises the actual anonymous consumer
path, and is not folded into T3’s write-token-scoped automated coverage). A public
package must not be flipped private later without a migration — that would break
every installed consumer’s pull.
Note: pull-side auth (compass-native’s card, recorded for the dependency)
Section titled “Note: pull-side auth (compass-native’s card, recorded for the dependency)”On the linux/amd64 dogfood path: if the package is public, compass-stack’s
first-run podman pull needs no credential. If private, compass-native builds a
keychain-first pull-cred flow into compass-stack (per their DL-109 posture — on
dogfood’s Linux host that is a Secret Service keychain with a 0600 fallback;
cheap on their side, by their own assessment). With the package public (decided
above) this pull-cred flow is not needed for dogfood — it stays a private-
visibility contingency only, recorded so the dependency is explicit if
visibility ever changes.
Global Constraints
Section titled “Global Constraints”- Conventional Commits; this design PR is
docs(platform): …with theCo-authored-by: Matt Wilkinson <matt@sealedsecurity.com>trailer. - No AI-product names; no planning metadata in source. SEA-#### appears only as tracking refs in this record.
- Comments and docs explain non-obvious WHY (compass
AGENTS.md). - The image build stays OFF the hot
up/PR path: publish is main-only +workflow_dispatch, mirroring the opt-indogfood:agent-imageposture (devenv.nix:345-348— “NOT wiredafterinto up — the image closure is large”). - Reproducibility: the published
:git-<sha>and the local dogfood load are copies of the SAME nix derivation — both flow throughcontainer_build agent(forks/devenv/devenv/src/devenv/container.rs:55) and diverge only in skopeo destination. :git-<sha>tags are immutable: the publish script refuses to overwrite a tag whose remote config digest differs from the local spec’s, and a deliberate re-publish of the same sha is a no-op success only when the digests match. A registry error that is not an unambiguous manifest-unknown aborts (never falls through to an overwrite).- ONE-JOB doctrine (
ci.yml:4-23) is respected: the gate workflow is untouched; the publish workflow enumerates no moon projects and is not a required check.
T1 — Publish script
Section titled “T1 — Publish script”Add agent-image/publish.sh: build the spec, then push the tag list. Runnable
locally (with a PAT-backed skopeo login) and from CI identically.
- Build:
nix run path:../forks/devenv#devenv -- container build agentexecuted with cwdagent-image/(the same fork-pinned invocation shape asdogfood:agent-image,devenv.nix:349-354); capture the printed image-spec store path. - Skopeo:
nix run path:../forks/nix2container#skopeo-nix2container --(exposed atforks/nix2container/flake.nix:31;pkgs.skopeo’smeta.mainProgramsurvives theoverrideAttrsatdefault.nix:22, sonix runresolvesbin/skopeo). - For each requested tag, in order (
git-<sha>beforelatest, so the immutable pin lands before the moving tag), agit-*tag runs the immutability guard first:- Read the remote config digest.
skopeo inspect --raw docker://…:<tag> | jq -r .config.digest. Plainskopeo inspect’s.Digestis the manifest digest, which skopeo may re-serialize on copy;--raw+.config.digestis the rootfs-pinning identity the fork itself compares (GetConfigDigest,forks/nix2container/nix/image.go:40-47) and survives a copy verbatim. - Error taxonomy — never fall open. If the inspect fails, distinguish
manifest-unknown (tag genuinely absent: GHCR returns
manifest unknown/ HTTP 404 — parse stderr for it) from any other failure (auth 401/403, network 5xx, rate-limit). Manifest-unknown → the tag is free, proceed to copy. Any other error → abort non-zero; a transient registry hiccup must never be read as “tag absent, safe to overwrite”. - Compare. Read the local spec’s config digest the same way
(
skopeo inspect --raw nix:$SPEC | jq -r .config.digest). Equal → idempotent re-run, skip the copy (no-op success). Differ → fail with a “tag exists — immutable” error.
- Read the remote config digest.
- Copy:
skopeo --insecure-policy copy nix:$SPEC docker://ghcr.io/sealedsecurity/compass-agent:<tag>(--insecure-policymatches the module’s own copy invocation,containers.nix:307). - Post-copy assert. After each copy, re-inspect the pushed tag’s config digest and assert it equals the local spec’s — upgrading the guarantee from “skopeo exited 0” to “the artifact I built is the artifact at that tag”, and closing the check-then-copy TOCTOU window’s damage (a racing overwrite is detected, not silently accepted).
- Auth:
--authfile "$REGISTRY_AUTH_FILE"on every skopeo call (never an ambient-only login — see the auth-file pin above). - Verify the guard’s linchpin first. The whole guard (local-digest read,
compare, post-copy re-inspect) rests on
skopeo inspect --raw nix:$SPECworking as a standalone inspect through the fork’snix:transport — the one claim resting on a build-time-fetched patch (forks/nix2container/default.nixpulls the transport fromgithub.com/nlewo/container-libs) rather than an in-tree file. The first implementation spike must confirmskopeo inspect --raw nix:$SPEC | jq -r .config.digestsucceeds against the patched skopeo before the guard is built on it, so a broken assumption fails at T1, not at first publish. (Strongly expected to hold — the samenix:ImageSource already backsskopeo copy,containers.nix:307— but out-of-tree, so verify.)
Interfaces:
- Consumes: cwd
agent-image/;forks/devenv+forks/nix2containerflakes; git HEAD for the sha tag; an existingskopeo login ghcr.iosession. - Produces: pushed
docker://ghcr.io/sealedsecurity/compass-agent:git-<sha12>anddocker://ghcr.io/sealedsecurity/compass-agent:latest; exits non-zero on immutability violation or copy failure. - CLI:
./publish.sh [tag …](no args = the default two-tag set; the GA release tag later becomes./publish.sh git-<sha> v<semver> latest).
T2 — Publish workflow
Section titled “T2 — Publish workflow”Add .github/workflows/publish-agent-image.yml:
on: push: branches: [main]with thepaths:filter from the Approach (globs listed there, with a header comment tying each glob to the closure input it tracks), plusworkflow_dispatch.workflow_dispatchref guard.workflow_dispatchruns on any branch, so the publish job is guardedif: github.ref == 'refs/heads/main'— a dispatch from a feature branch must never mint a:git-<sha>for unmerged code nor move:latestoff main. (Main pushes satisfy the guard trivially.)permissions: contents: read, packages: write(workflow-level; nothing else).concurrency: group: publish-agent-image, cancel-in-progress: false— publishes serialize rather than cancel, so an in-flight:latestmove is never half-superseded (a superseded push skips its tag, per the Approach — never a torn tag pair).- Steps: checkout (default depth — the script needs only HEAD);
cachix/install-nix-actionpinned to the same sha +extra_nix_configblock asci.yml:227-245(same substituters, same reviewed-file trust rationale — neveraccept-flake-config);skopeo login ghcr.io -u ${{ github.actor }} --password-stdin <<< "$GITHUB_TOKEN"via the fork’s skopeo; runagent-image/publish.sh. timeout-minutes: 90(the image closure is the same heavy build that sizes the CI job’s timeout,ci.yml:93-95).
Interfaces:
- Consumes:
agent-image/publish.sh(T1);secrets.GITHUB_TOKEN;github.actor. - Produces: the two pushed refs above on every closure-affecting main push; a manually dispatchable republish of HEAD.
- Not a required check; never triggered by
pull_request.
T3 — Verification
Section titled “T3 — Verification”Two layers, one automated + one documented smoke:
- In-workflow (end of T2’s job), all with
--authfile "$REGISTRY_AUTH_FILE":skopeo inspect docker://…:git-<sha12>must succeed — proves the artifact is resolvable from GHCR, not merely that the copy exited 0. Assert.Architectureand.Osmatch the build platform (the cheapest tripwire for a platform- contract regression, see thelinux/amd64platform Decision above), and assert:latest’s config digest equals:git-<sha12>’s (one inspect each — proves the two-copy pair landed coherently). Automated coverage ends here: at “the manifest is resolvable with the workflow’s own write-scoped token”; it does not exercise the visibility-dependent anonymous pull (public, so anonymous pull is the contract — an unauthenticatedskopeo inspectis the truest check, notable as a T3 follow-up) nor a real pull-and-run. - Documented smoke (in the workflow header comment and the T4 doc fold): on a
runner host,
podman pull ghcr.io/sealedsecurity/compass-agent:git-<sha12>then startcompass-runner --image ghcr.io/sealedsecurity/compass-agent:git-<sha12>and drive one provision — exercising the exact consumer seam (go/cmd/compass-runner/main.go:44-45,111-114). This is the acceptance check compass-native T2 repeats from the pull side.
Interfaces:
- Consumes: the pushed refs from T2; a rootless-podman host with the runner built.
- Produces: a green publish run implies GHCR-resolvability; the documented
smoke procedure lives in the workflow header +
build-and-ci.md.
T4 — Doc fold
Section titled “T4 — Doc fold”After this record freezes and T1–T3 land: add a short “Publishing the agent
image” section to docs/architecture/build-and-ci.md (which already owns the
CI architecture narrative) covering the ref/tag contract, the one-derivation
reproducibility guarantee, the separate-workflow rationale (least privilege,
not a gate), and the smoke procedure. This record stays the design of record;
build-and-ci.md carries the durable operational description.
Interfaces:
- Consumes: the merged T1–T3 artifacts; this record.
- Produces: an updated
docs/architecture/build-and-ci.md; no code changes.
- T1:
agent-image/publish.sh— spec build + immutable-guarded two-tag skopeo push. - T2:
.github/workflows/publish-agent-image.yml— main-only, path-filtered,workflow_dispatch, least-privilegepackages: write,GITHUB_TOKENauth. - T3: in-workflow
skopeo inspectverification + documentedpodman pull/--imagesmoke. - Platform contract:
linux/amd64single-arch (settled per compass-native’s frozen record #1073); T3 asserts.Architecture/.Osas the tripwire. macOS/aarch64multi-arch is a forward add in the GA native-packaging record, not here. - T4: fold the durable publish description into
docs/architecture/build-and-ci.md. - One-time (whoever lands T2): set the GHCR package public after the first push (Matt’s ruling — compass is OSS).
- Deferred hardening (T4-adjacent): gate the image build pre-merge so an
image-build break is caught at PR time instead of in the post-merge
publish. Implemented as the
compass-agent-imagemoon project (agent-image/moon.yml), which realises the full image in the CI gate on any PR affecting the image closure. An earlier eval-only variant (nix evalthe drv without realising) was superseded by Matt’s ruling to build the full image: eval-only caught only the eval-time bun-drift assert, whereas the moon build also catches realise-time breaks (FOD-hash invalidation, a broken bundle), and reuses the same nix-in-gate posture the vendored forks already establish.
Open Questions
Section titled “Open Questions”- [Resolved] Record placement. This record lives in the sealed design
corpus (
docs/designs/platform/), the wave’s canonical home for frozen design records, beside the othercompass-*records. T4 folds the durable operational content into the compass repo’sdocs/architecture/build-and-ci.mdonce implemented; this record stays the design of record in sealed. - [Non-load-bearing] GA release tag. SEA-1687 will likely want
:v<semver>alongside:git-<sha>; T1’s tag-list CLI makes that a no-redesign later add. Deferred to GA planning. - [Non-load-bearing]
:git-<sha>tag retention. Immutable per-sha tags accumulate on GHCR unbounded. No retention policy now (cost is negligible at this volume); a later cleanup (keep last N, or prune unreferenced shas) is a no-redesign add. Noted so it is a deliberate deferral, not an oversight.