kolu
Docs

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.