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:
- Fetches
bundle.json(this document) + a siblingbundle.json.sha256integrity hash froms.seglamater.app. - Writes both into your install dir under
./scripts/bundle.json. - Invokes
./scripts/install.sh --bundle ./scripts/bundle.jsonto 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
canaryfor new customers,stablefor 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.shtakes the first ofusername/userand oftoken/passwordthat 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.appcarries 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-updaterrefuses 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.shinto./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:
- Bundle SHA —
bundle.jsonis delivered alongside a siblingbundle.json.sha256.install.shreads both, recomputes the SHA-256 ofbundle.json, and aborts if it doesn't match. A missing.sha256is 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.sha256sumis a prerequisite for the same reason — while it was optional, a host without it completed an install with the bundle hash unverified. - Cosign pubkey pin — the cosign public key fetched from
pubkey_urlis hash-checked againstcosign.pubkey_sha256before being written tosecrets/cosign_pub. A mismatch aborts install immediately (no.tmpleft around to confuse retries). - Image digest pin —
install.shpullsregistry.ref_with_digest(theimage@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 afterinstall.shconsumes 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.