fix(catalog): generate against the fleet's trust floor, not the repo's host

sign-catalog.sh refused to sign a freshly regenerated catalog:

  REFUSING: the catalog names registry hosts the deployed fleet does not
  trust.  source.archipelago-foundation.org — 78 image refs

It was right, and the check earned its place. apps/*/manifest.yml moved to
the public domain in 8e814ca0, but releases/registry-trust-floor.json still
lists only docker.io, ghcr.io, localhost and the OVH host — the migration is
ship-binary -> confirm-fleet -> promote-floor -> regenerate, and only the
first move happened. A full regen therefore produced a catalog naming a host
no deployed binary trusts, which would have failed every install in the field
with "not from a trusted registry". Verifying that an image PULLS from the
domain (it does, including from a fleet node) does not verify that shipped
binaries ACCEPT it — different checks.

Two fixes, no manifest churn:
- image-versions.sh honours a caller-supplied ARCHY_REGISTRY instead of
  overwriting it, so generation can target a host inside the floor.
- the generator retargets OUR registry host inside embedded manifests to
  whatever it is generating against, leaving docker.io/ghcr.io refs alone.
  REGISTRY moves above the embed block, which now depends on it.

The repo keeps naming the public domain; only the generated artifact is
pinned to what the fleet can actually use. When the floor is promoted,
generating against the domain becomes a no-op here.

Verified: check-catalog-registry-trust.py passes, 11 docker.io/ghcr.io refs
untouched, and the diff against the signed catalog is exactly one app —
bitcoin-knots, :latest/20260508 -> :29.3.knots20260210 across all four fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-09 04:52:18 -04:00
co-authored by Claude Opus 5
parent ec8d88a692
commit 96acd388d7
2 changed files with 44 additions and 3 deletions
+34 -2
View File
@@ -143,6 +143,39 @@ for app_id, comps in STACK.items():
# into an AppManifest. Apps not already in SINGLE/STACK get a new entry whose
# version comes from the manifest. A bad embed is harmless: the node validates and
# falls back to its disk manifest.
# Embedded manifests must name a registry the DEPLOYED fleet trusts, which is
# not necessarily the one the repo names. apps/*/manifest.yml moved to the public
# domain in 8e814ca0, but releases/registry-trust-floor.json still lists only the
# OVH host — the migration is ship-binary -> confirm-fleet -> promote-floor ->
# regenerate, and the later steps have not happened. Embedding the repo's host
# verbatim produced a catalog naming 78 untrusted refs, which would have made
# every install in the field fail with "not from a trusted registry". The signer
# refused it, which is how this was caught.
#
# So rewrite OUR registry host to whatever REGISTRY is generating against, and
# leave every other host (docker.io, ghcr.io, ...) untouched. When the floor is
# promoted, generating against the domain becomes a no-op here.
REGISTRY = os.environ.get("ARCHY_REGISTRY", "source.archipelago-foundation.org/lfg2025")
_KNOWN_ARCHY_REGISTRY_HOSTS = (
"source.archipelago-foundation.org/lfg2025",
"146.59.87.168:3000/lfg2025",
)
def _retarget_registry(node):
if isinstance(node, dict):
return {k: _retarget_registry(v) for k, v in node.items()}
if isinstance(node, list):
return [_retarget_registry(v) for v in node]
if isinstance(node, str):
for host in _KNOWN_ARCHY_REGISTRY_HOSTS:
if host != REGISTRY and node.startswith(host + "/"):
return REGISTRY + node[len(host):]
return node
return node
embedded = 0
apps_dir = os.environ.get("APPS_DIR")
if os.environ.get("EMBED_MANIFESTS") and apps_dir:
@@ -159,7 +192,7 @@ if os.environ.get("EMBED_MANIFESTS") and apps_dir:
continue
entry = apps.setdefault(str(app_id), {})
entry.setdefault("version", str(app.get("version", "")) or "0")
entry["manifest"] = data
entry["manifest"] = _retarget_registry(data)
embedded += 1
# Multi-version support (docs/bitcoin-multi-version-design.md §3 Phase 1):
@@ -172,7 +205,6 @@ if os.environ.get("EMBED_MANIFESTS") and apps_dir:
# image.sh (Phase 0) publishes more tagged images, e.g.:
# {"version": "30.0", "image": f"{REGISTRY}/bitcoin:30.0"},
# {"version": "27.2", "image": f"{REGISTRY}/bitcoin:27.2", "deprecated": True, "eol": "2026-12-31"},
REGISTRY = os.environ.get("ARCHY_REGISTRY", "source.archipelago-foundation.org/lfg2025")
VERSIONS = {
# Curated Core set (latest patch per major, current → 25). Images built +
# verified (SHA-256 + OpenPGP, fail-closed) and pushed by
+10 -1
View File
@@ -10,7 +10,16 @@
# to verify against the registry.
# Archipelago app registries (primary + fallback)
ARCHY_REGISTRY="source.archipelago-foundation.org/lfg2025"
#
# Honour a caller-supplied ARCHY_REGISTRY instead of overwriting it. Catalog
# generation MUST be able to target a host inside releases/registry-trust-floor.json
# — the hosts binaries already deployed to the fleet are known to trust. This
# file's default is the public domain, which the fleet does NOT trust yet: the
# migration is ship-binary -> confirm-fleet -> promote-in-floor -> regenerate,
# and only step 0 has happened. Generating the catalog against an untrusted host
# makes every install in the field fail with "not from a trusted registry"; the
# signer refuses to sign such a catalog, which is how this was caught.
ARCHY_REGISTRY="${ARCHY_REGISTRY:-source.archipelago-foundation.org/lfg2025}"
# No fallback registry: the old tx1138 registry host was retired (2026-06-13); empty disables the fallback path.
ARCHY_REGISTRY_FALLBACK=""