Skip to content

Bundle spec — what bundle.json is and how install.sh consumes it

A bundle is the per-customer, per-release configuration document Seglamater ships to you alongside the installer. It pins exactly which image digest your install pulls, on which channel you'll receive future updates, and which cosign public key your updater trusts. The bundle is small — a single JSON object — but it is the source of truth for your install's identity from the moment you run install.sh.

The release-side authoring code lives in crates/hivemind-release/src/bundle.rs. The consumer side lives in scripts/install.sh (the validation block). When the two diverge, new installs break; v0.1.0 shipped a manifest-shaped document where the bundle should have been, and the first real install rejected it. The v0.1.1+ shape documented here is the one install.sh validates.

Where the bundle comes from

You receive the bundle via your invite URL, the time-limited bootstrap endpoint your support contact emails you. It is not one-shot: it carries both a use limit and an expiry (3 installs and 24 hours by default), and your invitation email states the exact moment it stops working.

curl -fsSL https://s.seglamater.app/i/<invite-id> -o hivemind-install.sh && bash hivemind-install.sh

The bootstrap script:

  1. Fetches bundle.json (this document) + a sibling bundle.json.sha256 integrity hash from s.seglamater.app.
  2. Writes both into your install dir under ./scripts/bundle.json.
  3. Invokes ./scripts/install.sh --bundle ./scripts/bundle.json to render .env + bring up the Compose stack with the pinned image.

You never edit bundle.json by hand. If anything inside it needs to change (you're switching channels, a new release is out, your support tier changed), you ask your support contact to issue a fresh invite and re-run the bootstrap.

Lifecycle: when is a bundle minted?

  • First install — Seglamater issues an invite scoped to your organization. The bundle on the other side of that invite is pinned to a specific image digest on your initial channel (usually canary for new customers, stable for established ones).
  • Major upgrade — when you opt in to a major version Seglamater may cut you a fresh bundle so the pinned image digest in your install dir matches the new line.
  • Channel change — switching canary → beta → stable (or back) is bundle-mediated. There is no in-app channel switcher today.

Day-to-day point releases do not require a new bundle. The managed-update sidecar polls updates.seglamater.app on your bundle's pinned channel + applies the new pinned image digest in-place. See upgrade.md.

Fields

The shape install.sh validates is schema_version: 1. The required fields are the four at the top of install.sh's python validator (line 290 in scripts/install.sh, kept in sync with the schema):

Required

Field Type Validation Why
schema_version integer MUST equal 1 Lets the installer fail fast on a v2 bundle it can't read.
app string MUST equal "hivemind" Prevents accidentally piping an atlas or vantage bundle here.
version string Free-form, conventionally semver Stamped into .env as HIVEMIND_VERSION, which the hivemind-updater sidecar reads. Not what /api/v1/health reports — see the install-effects table below.
channel string "canary", "beta", or "stable" What hivemind-updater polls. Bundle wins over the operator's --channel flag.

Optional but always present in real bundles

Field Type What
created_at string RFC-3339 timestamp the bundle was cut. Diagnostic.
manifest_url string Where the public release manifest lives — https://updates.seglamater.app/hivemind/releases/<version>/manifest.json. Used by cosign verify + by the updater poll.
registry.image string Floating-tag image ref, e.g. registry.seglamater.app/seglamater/hivemind-server:0.1.70.
registry.digest string sha256:<hex> — the pinned image digest. This is the tamper-evident pin.
registry.ref_with_digest string Convenience: <image>@<digest>. What install.sh actually pulls.
registry.url string Registry host for the docker login, e.g. registry.seglamater.app. Any http(s):// prefix is stripped. Defaults to registry.seglamater.app.
registry.username string Registry username. Required, with token, for the bundle to be installable. Alternate spelling: user.
registry.token string Registry pull token (conventionally a Forgejo read:package token scoped to the customer). Required, with username. Alternate spelling: password.

The credential-bearing rule

The registry refuses anonymous callers: GET /v2/ returns 401, and its token service will not issue an anonymous token even for a bare pull scope. There is no unauthenticated path to the image, so there is no "anonymous pull" fallback for a bundle to rely on.

That makes registry.username + registry.token load-bearing rather than optional. A bundle that pins an image but carries neither is not a bundle that usually works and occasionally doesn't — it is one that cannot install. install.sh therefore refuses before the pull, naming the missing credentials, instead of dying at docker compose pull on a 401 that cannot say which of a dozen things went wrong.

Two rules keep the ends aligned:

  • Both spellings, one test. install.sh takes the first of username/user and of token/password that holds a non-empty string. s-updater's mint-time validator (app/credbind.py) applies the identical test, so a bundle cannot pass the mint and then fail the install.
  • Generic bundles are not installable. The release bundle published to updates.seglamater.app carries no credentials — the publisher's own type (crates/hivemind-release/src/bundle.rs, BundleRegistry) has image/digest fields and no username or token, so it is structurally incapable of carrying them. Only a per-customer bundle minted from an invite carries credentials. s-updater refuses to serve a generic bundle against an invite for exactly this reason.

install.sh does not refuse when credentials live somewhere else it can see: a non-empty secrets/registry_creds left by a previous install, or an existing docker login for the registry host, both let the install proceed. The refusal fires only when an image is pinned and none of the three sources exists. | cosign.pubkey_url | string | Where to fetch the cosign public key (default https://updates.seglamater.app/.well-known/keys/hivemind.pub). | | cosign.pubkey_sha256 | string | SHA-256 of the pubkey. install.sh fetches the key + asserts the hash matches before writing secrets/cosign_pub. A mismatch aborts install. | | support.email | string | The mailbox for your install's tier — usually support@seglamater.com. | | support.docs_url | string | Where the published customer docs live. | | files | array of {path, role} | What the bootstrap fetched alongside the bundle. Informational; install.sh doesn't re-fetch from this list. Common roles: compose, env-template, license. | | migrations.added | integer | Database migrations this release adds, counted from history when the release is cut (the same count the release notes state). Your Updates page reads it before an apply: a release that adds migrations cannot be rolled back by switching images. Absent on bundles cut before this field existed, which the Updates page reports as "not declared", never as zero. | | migrations.since_tag | string | The previous release tag the count starts from, e.g. v1.2.3. | | release_notes_url | string | The public release notes page. The Updates page links it before an apply. |

What's NOT in the bundle (intentional)

  • No secrets, with exactly one stated exception. Database passwords, admin API keys, JWT keys, the integration encryption key, cookie secrets — all of those are generated locally by install.sh into ./secrets/ on your host and never travel over the network. Seglamater never sees them. This is the security model: even a compromised bundle delivery channel cannot leak your install's keys.

The exception is registry.token, the registry pull credential. It has to travel, because the host must authenticate to a registry that refuses anonymous callers before it has any of your install's other material. It is scoped to pulling your images and nothing else (conventionally a Forgejo read:package token), it is per-customer and revocable, and it is the only secret in the bundle. Treat a per-customer bundle.json as sensitive and do not re-share it. - No LLM API key. Hivemind is BYOK; you set HIVEMIND_LLM_API_KEY in .env post-install. See byok-llm.md. - No URL / hostname your install runs at. HIVEMIND_PUBLIC_URL is set by you in .env; the bundle has nothing to say about your reverse-proxy hostname.

Integrity + signing

Three layers of integrity gate the install:

  1. Bundle SHA — bundle.json is delivered alongside a sibling bundle.json.sha256. install.sh reads both, recomputes the SHA-256 of bundle.json, and aborts if it doesn't match. A missing .sha256 is a hard failure, not a warning (HIVE-c745bb51): a bundle with no sibling checksum cannot be integrity-checked, and accepting one meant installing from an unverified bundle. sha256sum is a prerequisite for the same reason — while it was optional, a host without it completed an install with the bundle hash unverified.
  2. Cosign pubkey pin — the cosign public key fetched from pubkey_url is hash-checked against cosign.pubkey_sha256 before being written to secrets/cosign_pub. A mismatch aborts install immediately (no .tmp left around to confuse retries).
  3. Image digest pin — install.sh pulls registry.ref_with_digest (the image@sha256:… form). Docker verifies the manifest digest at pull time; substituting a different image on the registry can't slip through.

The bundle itself is also cosign-signed on the publisher side (see dist/bundle.json.sig in the cut). v1 install.sh does not yet verify that signature — it relies on the SHA-256 + the bootstrap delivery channel. The bundle-cosign verification step is on the roadmap; the artifacts are already published so the verification can be turned on without a re-cut.

What install.sh actually does with each field

Tracing the consumer code: scripts/install.sh starts in argument parsing, hits step 2 ("Validating bundle"), runs python against bundle.json to validate + extract a small set of fields into shell variables, then proceeds through env rendering + image pull.

Bundle field Effect on install
app Asserted equal to "hivemind".
schema_version Asserted equal to 1.
version Written to .env as HIVEMIND_VERSION=<version>, read by the hivemind-updater sidecar. Not surfaced at /api/v1/health: the shipped docker-compose.yml does not pass this name to hivemind-server, and both /api/v1/health and the boot log report the server's compiled version, so the two can differ.
channel Written to .env as HIVEMIND_CHANNEL=<channel> (not HIVEMIND_UPDATER_CHANNEL — that variable doesn't exist). The updater sidecar polls that channel's manifest.
registry.ref_with_digest docker pull <ref_with_digest> — the actual server image fetch.
registry.username + registry.token Written to secrets/registry_creds for the updater sidecar, AND used for a docker login before the install-time pull (the pull uses the docker credential store, not that file). Absent with an image pinned and no other credential source: the install refuses before the pull.
registry.url The host passed to docker login and stored as serveraddress. Defaults to registry.seglamater.app.
registry.updater_ref_with_digest (or registry.updater_image as a fallback) Written to .env as HIVEMIND_UPDATER_IMAGE=<value> if present. Digest-pinned ref preferred over the mutable tag.
registry.runner_ref_with_digest (or registry.runner_image as a fallback) Written to .env as HIVEMIND_RUNNER_IMAGE=<value> if present. Empty on every ordinary bundle today — the release pipeline's publish gate for this image defaults closed.
cosign.pubkey_url + cosign.pubkey_sha256 Fetched + hash-checked into secrets/cosign_pub.
Everything else Available for diagnostic logging or future use; not load-bearing for the initial install.

If a required field is missing, install.sh prints bundle missing required fields: … and exits non-zero before touching docker. If schema_version isn't 1 or app isn't hivemind, it prints the same sort of error. The fail-closed posture is intentional: a bad bundle should never produce a half-installed stack.

Error modes + what they mean

Symptom What's wrong Fix
install.sh: bundle.json parse failed: … The file isn't valid JSON. Bootstrap pulled a corrupted file; re-run the invite. Don't hand-edit.
install.sh: bundle missing required fields: … One of schema_version / app / version / channel is absent. Bundle is malformed — ask your support contact for a fresh invite.
install.sh: bundle schema_version != 1 You're on an installer that pre-dates a v2 bundle (forward), or the bundle was issued for a different installer line. Upgrade install.sh (re-run the invite) or ask for a v1 bundle.
install.sh: bundle app != 'hivemind' You piped an atlas / vantage / other bundle through hivemind's installer. Re-fetch the right invite.
cosign public key SHA-256 mismatch The fetched pubkey doesn't match the pin in the bundle. Investigate before re-running. This is either an in-flight MITM or a stale bundle against a key rotation. Don't override; contact security@seglamater.com.
Bundle SHA-256 mismatch warning bundle.json was modified after bundle.json.sha256 was generated. Re-bootstrap from the invite. Don't edit the bundle.

When to ask Seglamater for a fresh bundle

  • You're switching channel and want it to stick.
  • You believe the bundle was corrupted in delivery.
  • Cosign pubkey rotation happened on Seglamater's side (we'll tell you, but if you see the SHA mismatch first the fix is a new bundle).
  • A major version cut you opted in to that the v1 updater couldn't apply in-place (rare — most upgrades are point-release in-channel).

For routine point releases you do not need a fresh bundle — the sidecar pulls the new pinned image on the channel your existing bundle already pins.

See also

  • bundle-schema.json — the formal JSON Schema (draft 2020-12) for programmatic validation in your tooling.
  • bundle-example.json — a complete, realistic bundle you can use as a reference when reading your own.
  • install.md — the install walkthrough; what happens before, during, and after install.sh consumes the bundle.
  • upgrade.md — what the channel + image digest in your bundle drive through the managed-update path.
  • license.md — the license terms governing your use of the product the bundle gives you access to.