How to publish your daemon content-addressed
A remote dial normally needs the host’s Nix to trust something — your ssh
login (trusted-users) or your cache’s key — before it accepts your daemon’s
closure. A content-addressed store path needs neither: its name is the
hash of its contents, so every Nix daemon accepts it from anyone. This guide
publishes your daemon that way, so provisionAgent has the host pull it
straight from your cache, with no trust and no bytes through your machine.
You need a binder built with mkProvenAgentSource
(@kolu/surface-daemon), an
attic cache already named in your agent
flake’s nixConfig, and GitHub Actions.
1. Declare where your builds are published
Give mkProvenAgentSource a closureIndex — a branch of a GitHub repository
that will hold small JSON files naming each build:
agentSource = mkProvenAgentSource {
# …root, fileset, pkgs, commitHash, agents, flakeNix as before…
closureIndex = { repo = "you/your-app"; branch = "ca-index"; };
};
It lands in the baked binary-cache.json beside your caches, with the raw URL
derived for you. The argument is required: a source that publishes nothing says
closureIndex = null.
2. Publish from CI
Add a job that runs the publish-closure action on each system you serve. It
must be the job’s only Nix installer — the action installs the Nix it rewrites
with.
publish-closures:
needs: build-and-push # your job that pushes the normal closures
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
permissions:
contents: write # it pushes the index branch
steps:
- uses: actions/checkout@v4
- uses: juspay/kolu/.github/actions/publish-closure@master
with:
source: .#agent-flake-source # your binder's baked source
installables: my-daemon # attribute names under it
latest: ${{ github.ref == 'refs/heads/main' }}
smoke: | # one line per installable — required
my-daemon=bin/my-daemon --version
attic-nixpkgs: github:NixOS/nixpkgs/<a pinned rev>
attic-token: ${{ secrets.ATTIC_TOKEN }}
Without source, pass sidecar (the declaration) and, optionally, rev — the
key entries are written under. Omitted, rev is the commit being published
(github.event.pull_request.head.sha || github.sha); never pass a bare
github.sha on a pull request, where it is GitHub’s merge commit and is
refused. It refuses to run in a repository other than your declared closureIndex.repo,
and an installable with no smoke line fails the run rather than skipping.
For each attribute it builds the closure, rewrites it content-addressed (not
on macOS), runs the program out of the rewrite, pushes both forms to your
cache, imports the rewrite back into a throwaway store to verify it, and only
then writes <rev>/<system>/<attr>.json to the index branch. On a pull request
it does everything except write the index.
3. Dial as usual
Nothing changes at the call site. A dial from a published revision now reads
its own entry and has the host pull it; the progress log says
landed … from the closure index. A dial from anything else — a dirty
checkout, a revision CI never published — reads a 404 and takes the usual
route (rewrite, ship, or build on the host), saying so.