Skip to content

Deployment bundle specification

The deployment bundle is the artifact a Seglamater operator hands to a paying customer to deploy or upgrade Chatalot. It captures the per-customer configuration that install.sh needs to land a working stack on the managed-update pipeline.

This document is the canonical specification of the bundle format. The machine-readable schema is at bundle-schema.json in this directory; a working example is at bundle-example.json.

Lifecycle

operator (mint) → customer (or AI agent) → install.sh → running stack
                   long-lived bundle file
                   used at install + future re-installs + DR rebuild

A bundle is minted once per customer instance by the vendor-side mint-client-bundle.sh script (the Forgejo registry token is created fresh each mint; the bundle is the durable record of that mint). It travels to the customer with the rest of the deployment package (zip archive containing bundle.json, WELCOME.md, LICENSE.txt, the cosign public key, and rendered email body). The customer or their AI agent runs install.sh --bundle bundle.json on the target VPS. install.sh consumes the bundle, generates per-instance secrets locally, pulls the pinned images, brings up the stack on the managed-update pipeline, and exits.

The bundle is kept by the customer for as long as they operate the instance. It is consulted on:

  • Initial install
  • DR rebuild on a fresh host (same instance, same data)
  • Re-application after major host migration
  • Support investigations (the operator asks for the instance_id to correlate)

When a customer offboards, the registry token referenced by the bundle is revoked. The bundle file itself is no longer functional but still useful as an audit record for both sides.

Format

JSON, UTF-8, no BOM, single object at the root, keys in the order documented in the schema. No comments (JSON spec). Pretty-printed for human readability is fine; install.sh consumes either form.

The JSON Schema (bundle-schema.json) is the authoritative shape definition. install.sh validates incoming bundles against it before doing any work and refuses to proceed on schema violation.

Top-level fields

Field Why it exists
schema_version Hard versioning; install.sh refuses bundles with versions it doesn't understand. v1 is the only published version.
client_id URL-safe slug used in resource names — Forgejo token name suffix, bundle filename, log identifiers. Stable for the life of the customer relationship.
client_name Display name for humans. Free-form. Shown in the welcome doc and admin UI.
instance_id ULID for this specific deployment. Sortable. Surfaced to the customer for support reference. A customer with multiple instances has multiple bundles, each with its own instance_id.
created_at When the bundle was minted. Used for "is this bundle stale?" warnings and audit trails.
chatalot.* Settings written to the chatalot service environment during install.
registry.* Per-instance read-only registry credentials and image paths.
cosign.* Trust anchor for signed-release verification.
manifest_host Base URL for release-channel manifests.
support.* Support relationship metadata, surfaced in the welcome doc.

See bundle-schema.json for the full constraints on each field.

What is a secret in the bundle

The only field that grants any access is registry.token. It is:

  • A Forgejo read:package-scoped access token
  • Scoped to a single client's image stream by token name
  • Revocable in seconds (DB delete or web UI)
  • Read-only — cannot push images, cannot read repos, cannot do anything else

Loss of a bundle file is a recoverable event. Procedure:

  1. Operator revokes the named token (chatalot-<client_id>-readonly-registry)
  2. Operator mints a new bundle with a new token
  3. Operator delivers the new bundle through the same email pipeline
  4. Customer re-runs install.sh --bundle bundle-new.json on their host

There are no fields in the bundle whose loss would compromise the customer's data, the cryptographic identity of their instance, or the integrity of the chatalot release-signing chain. Per-instance JWT keys, the database password, the HMAC token between chatalot-server and the updater sidecar — all generated locally during install and never written to the bundle.

This is by design: the bundle is meant to be emailed, copied to a deploy host, archived in operator records, and possibly later seen by a third party during DR or audit. None of those workflows should require encryption or guarded handling.

Bundle delivery

The bundle JSON travels inside a zip archive named chatalot-deployment-<client_id>-<YYYY-MM-DD>.zip. The archive contents are:

chatalot-deployment-acme-corp-2026-04-26/
  bundle.json              ← this spec
  WELCOME.md               ← rendered from scripts/onboarding-templates/WELCOME.md.tpl
  WELCOME.pdf              ← printable version of WELCOME.md
  LICENSE.txt              ← rendered from scripts/onboarding-templates/LICENSE.txt.tpl
  chatalot-public-key.pem  ← convenience copy of the cosign public key (install.sh re-fetches and re-verifies regardless)

The zip is sent to the customer via email from noreply@seglamater.com with the rendered EMAIL.txt template as the body. The Reply-To header is support@seglamater.com.

install.sh consumption rules

When install.sh --bundle <path> runs:

  1. Schema validation. Validates the JSON against bundle-schema.json. On failure, prints the schema violation and exits non-zero.
  2. Trust anchor verification. Fetches cosign.pubkey_url, computes its SHA-256, compares to cosign.pubkey_sha256. On mismatch, exits with a hard error referencing supply-chain compromise as the most likely cause.
  3. Pre-flight reachability. Resolves chatalot.public_url, confirms it points at the host's public IP (warns on mismatch), confirms port 443 is reachable.
  4. Secrets emission. Writes registry.token to secrets/registry_creds as Docker auth JSON. Saves cosign.pubkey_url-fetched public key to secrets/cosign_pub. Generates per-instance JWT keypair, DB password, HMAC updater_token, and totp_encryption_key locally — never reading any of these from the bundle.
  5. Compose ingestion. Writes chatalot.public_url, chatalot.admin_username, chatalot.update_channel, manifest_host to the chatalot service env. Pins both image fields by digest from the latest channel manifest.
  6. Stack up. Pulls images via docker login against registry.{url,username,token}, runs docker compose up -d, waits for healthchecks.
  7. Admin instructions. Prints the registration URL and admin_username so the customer registers the admin account within five minutes.

install.sh writes a copy of the bundle to secrets/bundle.json on the host (mode 0640) for future support reference and DR rebuilds. That copy contains the registry token. It is not committed to any source control.

Versioning policy

schema_version starts at 1. The version increments on:

  • Removal of a required field
  • Addition of a new required field
  • Change in the meaning or constraint of an existing field

Backwards-compatible additions (new optional fields, new enum values whose default behaviour is sensible, looser constraints) ship inside schema_version: 1 without bump.

install.sh versions support a defined range of bundle versions. Older install.sh against newer bundle: refuse with a clear error advising operator to upgrade the install script. Newer install.sh against older bundle: support if the bundle is one major version behind; refuse otherwise.

Producing a bundle

Bundles are produced by mint-client-bundle.sh (vendor-side; not committed in this repo). That tool:

  1. Accepts customer info via flags
  2. Generates a fresh instance_id (ULID, current timestamp prefix)
  3. Calls the Forgejo API to mint a read:package token named chatalot-<client_id>-readonly-registry
  4. Files the token in Vaultwarden as Forgejo Registry Token - chatalot-<client_id>-readonly (Deploy collection)
  5. Files an onboarding ticket in Plane CHAT project
  6. Renders the bundle JSON from the operator inputs + minted token + canonical cosign SHA
  7. Validates the bundle against this schema
  8. Renders WELCOME.md, WELCOME.pdf, LICENSE.txt, EMAIL.txt from the templates at scripts/onboarding-templates/
  9. Assembles the zip archive
  10. Sends the email via send-bundle-email.sh (which talks to Proton Bridge on Seg-VPS)

Until mint-client-bundle.sh ships, a hand-built bundle is acceptable for the first customer. Validate it manually with python3 -m jsonschema -i bundle.json bundle-schema.json (or jsonschema-cli, or ajv-cli) before sending.

Security model summary

  • Bundle is plain text. Anyone with the file can pull the customer's image stream until the token is revoked.
  • The cosign public-key SHA pinning prevents a malicious manifest host from serving a different signing key during install.
  • Per-instance secrets are generated on the host. The bundle never sees them. A bundle leak does not compromise the running instance's cryptographic identity.
  • Token revocation is a one-line operation: DELETE FROM access_token WHERE name = 'chatalot-<client_id>-readonly-registry' in the Forgejo DB, or via the web UI.
  • The bundle does NOT include the cosign signing private key or any vendor-side credential. Bundle exposure is per-customer; vendor-side compromise requires a separate attack surface.