Skip to main content

Host your own pack registry

An emisar pack registry is not a service — it's a static file layout over HTTPS. Anything that serves files can be one: a GCS or S3 bucket, MinIO, plain nginx, an artifact store behind your VPN. Author packs for your own tooling, publish them to infrastructure you control, and install them across your fleet with the same hash-pinned flow the public registry uses. This isn't a side door — the public registry is built and published by the exact tool documented here, from our CI, on every packs change.

Distribution and trust are deliberately separate: trust never rests on the registry. Installs pin a sha256 content hash, the runner re-hashes every pack at load time, and the portal requires an operator to trust each pack@hash per account before its actions can run. A registry — ours or yours — only moves bytes.

  1. 1

    Author and validate your packs

    Packs are directories of YAML — the authoring guide walks the format, and the YAML reference covers every field. Validation runs the same loader and hash path the runner enforces:

    emisar pack validate ./billing-tools
  2. 2

    Get packctl

    packctl builds and publishes registries. It's deliberately a separate binary from emisar: your fleet hosts never carry publish code or publisher credentials — only the workstation or CI job that builds the registry does. It shares the runner's pack loader, so published content hashes match emisar pack validate byte-for-byte.

    go install github.com/andrewdryga/emisar/runner/cmd/packctl@latest
  3. 3

    Build the registry tree

    --base-url is wherever you'll host it; tarball URLs in the catalog join onto it:

    packctl catalog build --packs ./packs --out ./dist \
      --base-url https://packs.acme.internal

    The output is the whole registry — immutable, content-addressed artifacts plus two mutable pointers:

    v1/catalog.json                                  latest catalog (mutable pointer)
    v1/catalog/<sha256>.json                         immutable catalog snapshot
    v1/suggest.json                                  lean suggest index (mutable pointer)
    v1/schemas/*.json                                catalog + authoring schemas
    v1/packs/<id>/<version>/<sha256>/pack.tar.gz     immutable pack tarball
  4. 4

    Host it anywhere static

    GCS is native — immutable objects upload precondition-protected so existing bytes are never overwritten, and the pointers flip last:

    GOOGLE_OAUTH_ACCESS_TOKEN=$(gcloud auth print-access-token) \
      packctl catalog publish --dir ./dist --bucket acme-pack-registry

    S3, MinIO, nginx, or anything else: the tree is plain files — sync it with any tool. Upload the immutable objects first, then the two pointers:

    aws s3 sync ./dist s3://acme-pack-registry \
      --exclude "v1/catalog.json" --exclude "v1/suggest.json"
    aws s3 cp ./dist/v1/suggest.json s3://acme-pack-registry/v1/suggest.json
    aws s3 cp ./dist/v1/catalog.json s3://acme-pack-registry/v1/catalog.json
  5. 5

    Install from it, fleet-wide

    Per install with --registry, or set the default for a host once with EMISAR_PACKS_REGISTRY. Pin the hash you reviewed — a tampered or mismatched copy is rejected before it reaches the runner:

    sudo emisar pack install billing-tools \
      --registry https://packs.acme.internal --hash sha256:<reviewed>
    sudo systemctl reload emisar
    
    # or fleet-wide via the environment:
    export EMISAR_PACKS_REGISTRY=https://packs.acme.internal
    emisar pack update --dry-run

    Then trust the pack once on the dashboard's Packs page — actions gate on that per-account trust, not on where the bytes came from. Edit the pack and its hash changes, which re-marks it pending until you re-trust.

  6. 6

    Publish the next version safely

    Always build against the currently-published catalog so history carries forward. --previous enforces the registry's core guarantee: a pack that changed bytes for an already-published id@version fails the build — bump the version instead. On a critical fix, --retire-older retires everything older than the version you're publishing in one operation.

    curl -fsS https://packs.acme.internal/v1/catalog.json -o current.json
    packctl catalog build --packs ./packs --out ./dist \
      --base-url https://packs.acme.internal --previous current.json

What your registry does — and doesn't — change

  • The cloud works unchanged. Runners advertise installed packs regardless of source; trust, policy, approvals, and audit apply identically to private packs.
  • Air-gap friendly. Mirror the public registry's tree — or build your own from a vendored packs/ checkout — onto an internal host. Nothing phones home.
  • Discovery stays public-registry-only. The dashboard's catalog browser and emisar pack suggest list the public index; your private packs appear in the portal the moment a runner advertises them, just not in those browse surfaces.
  • No CRL-style revocation. Retire versions with --retire-older and republish; hash pinning means an already-reviewed install never changes underneath you.