Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Docs are split by the question they answer:
- [0015 — Propose a post-cutoff bootc-only installer scope (Proposed)](adr/0015-bootc-only-installer-scope.md)
- [0016 — Version-2 bootc-only recipe contract (Accepted)](adr/0016-bootc-only-recipe-contract.md)
- [0017 — Publish Debian packages through frostyard/apt-publisher](adr/0017-publish-debian-packages-through-apt-publisher.md)
- [0018 — Read each image's core flatpak set from an OCI label (Proposed)](adr/0018-image-published-core-flatpaks-label.md)
- [0018 — Read each image's core flatpak set from an OCI label](adr/0018-image-published-core-flatpaks-label.md)

### Design

Expand All @@ -40,6 +40,7 @@ Docs are split by the question they answer:

- [Firn recipe schema (version 2; temporary bootc-v1 compatibility)](specs/recipe-schema.md)
- [Firn progress protocol (version 1)](specs/progress-protocol.md)
- [Core Flatpaks image label (version 1)](specs/core-flatpaks-label.md)

### Plans

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0006-install-time-offline-first-flatpaks.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 0006 — Install-time, offline-first flatpak provisioning on both paths

- **Status:** Accepted
- **Status:** Accepted; core-set source superseded by [0018](0018-image-published-core-flatpaks-label.md)
- **Date:** 2026-08-11

## Context
Expand Down
7 changes: 4 additions & 3 deletions docs/adr/0018-image-published-core-flatpaks-label.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 0018 — Read each image's core flatpak set from an OCI label

- **Status:** Proposed
- **Status:** Accepted
- **Date:** 2026-10-06

## Context
Expand Down Expand Up @@ -180,8 +180,9 @@ mechanism. When accepted, ADR-0006 gains a status note pointing here.
- Shapes: [design/architecture.md](../design/architecture.md) (system
configuration, flatpaks), [specs/recipe-schema.md](../specs/recipe-schema.md)
(`core_flatpaks`), [specs/progress-protocol.md](../specs/progress-protocol.md)
(`no_core_set`), a new core-flatpaks label spec created with the
implementation, [roadmap Phase 10](../plans/roadmap.md#phase-10)
(`no_core_set`, `core_flatpaks_label_invalid`),
[specs/core-flatpaks-label.md](../specs/core-flatpaks-label.md) (the label
contract), [roadmap Phase 10](../plans/roadmap.md#phase-10)
- Builds on: [ADR-0006](0006-install-time-offline-first-flatpaks.md)
(offline-first provisioning, whose core-set source this replaces),
[ADR-0010](0010-single-installer-iso-in-snosi.md) (snosi owns the ISO and
Expand Down
33 changes: 23 additions & 10 deletions docs/design/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Living document. Rationale:
boundary: firn ships binary + kiosk unit + contracts; snosi ships the
single installer ISO).
Contracts: [specs/recipe-schema.md](../specs/recipe-schema.md),
[specs/progress-protocol.md](../specs/progress-protocol.md).
[specs/progress-protocol.md](../specs/progress-protocol.md),
[specs/core-flatpaks-label.md](../specs/core-flatpaks-label.md).

<a id="prospective-post-cutoff-scope"></a>
## Current bootc-only recipe boundary
Expand Down Expand Up @@ -64,6 +65,16 @@ headless-only; the wizard instead accepts pasted keys and creates its own
private password files. The exact parity/delta table lives in the
[recipe schema](../specs/recipe-schema.md#interactive-wizard-parity).

The flatpaks page inspects the chosen image's
[core Flatpaks label](../specs/core-flatpaks-label.md) when it opens, with a
30-second timeout, using preflight's local-first selection without signature
verification, and again whenever the chosen image changes. It lists a
published set's app names beside the toggle; explains instead of offering
the toggle when the image publishes no set or a malformed one (and clears any
earlier answer); and keeps the toggle with an "available at install time"
note when inspection fails. The preview never enters the recipe: preflight
reads the label again at install.

Each interactive run owns a randomly named 0700 directory below `/run/firn`.
The reviewed recipe references only 0600 secret files in that directory, and
the wizard's canonical serializer returns the accepted review bytes directly
Expand Down Expand Up @@ -184,13 +195,14 @@ and record delimiters before the writer runs.
Flatpaks follow ADR-0006: copy from the medium's seeded
repo, download the remainder into the mounted target, report (never
silently drop) what was unreachable. With `core_flatpaks`, the core set
comes from first-setup's `core.json` in the deployment, or, when composefs
makes that unreadable ([ADR-0012](../adr/0012-bootc-install-from-ram-installer.md)),
from the ISO's `/usr/share/firn/core-flatpaks.json`; neither depends on the
image being installed.
[ADR-0018 (Proposed)](../adr/0018-image-published-core-flatpaks-label.md)
replaces both with an `org.frostyard.core-flatpaks` label each image
publishes ([roadmap Phase 10](../plans/roadmap.md#phase-10)).
comes from the selected image's `org.frostyard.core-flatpaks` label
([ADR-0018](../adr/0018-image-published-core-flatpaks-label.md),
[spec](../specs/core-flatpaks-label.md)). `preflight-image` reads it from
the same inspection that selects the source, before any disk write: a
malformed label fails the install there, and an image with no set is
reported as `no_core_set`. The flatpak step installs the recipe's explicit
apps, then the core set, each once. Firn no longer reads first-setup's
`core.json` or the ISO's `/usr/share/firn/core-flatpaks.json`.

### Trust

Expand Down Expand Up @@ -262,9 +274,10 @@ uses the same engine preflight as headless installation.
[ADR-0012](../adr/0012-bootc-install-from-ram-installer.md)
- Scope and contract: [ADR-0015 (Proposed)](../adr/0015-bootc-only-installer-scope.md),
[ADR-0016 (Accepted)](../adr/0016-bootc-only-recipe-contract.md),
[ADR-0018 (Proposed)](../adr/0018-image-published-core-flatpaks-label.md),
[ADR-0018](../adr/0018-image-published-core-flatpaks-label.md),
[roadmap Phase 9](../plans/roadmap.md#phase-9-proposed-post-cutoff-bootc-only-transition-bounded-cross-repo-not-started)
- Contracts: [specs/recipe-schema.md](../specs/recipe-schema.md),
[specs/progress-protocol.md](../specs/progress-protocol.md)
[specs/progress-protocol.md](../specs/progress-protocol.md),
[specs/core-flatpaks-label.md](../specs/core-flatpaks-label.md)
- Release and distribution: [ADR-0017](../adr/0017-publish-debian-packages-through-apt-publisher.md)
- Built in: [roadmap — Phases 1–7](../plans/roadmap.md)
73 changes: 38 additions & 35 deletions docs/plans/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,10 @@ this phase does not rewrite its historical A/B evidence or mark it done.
unsupported overrides wholesale and fall back to bootc-only built-ins.
Remaining rollout work includes inventory and migration of stored v1
recipes, release-date/expiry recording, and coordination with snosi on
its shipped catalog and ISO/tool payload. Sundog's ref/key and Snowfield's
scope need confirmation; Sundog is not in `builtinCatalog()`. Keep Firn's media boundary from
its shipped catalog and ISO/tool payload. Snowfield's scope needs
confirmation. Sundog is offered on the ISO through snosi's shipped
`/etc/firn/catalog.json`, which carries its ref and cosign key; it is not
in `builtinCatalog()`, which applies only without that file. Keep Firn's media boundary from
[ADR-0010](../adr/0010-single-installer-iso-in-snosi.md) and UEFI floor
from [ADR-0004](../adr/0004-single-installer-scope-and-support-matrix.md).
2. **Qualification and sole-path decision gate:** after the above changes,
Expand All @@ -288,7 +290,7 @@ this phase does not rewrite its historical A/B evidence or mark it done.
| --- | --- | --- |
| Snow | Secure Boot on + TPM present + `tpm2-luks-passphrase`; Secure Boot off + TPM absent + `none` | Published image ref and cosign key; published snosi ISO build; device identity; install-to-login and hostname/user/config checks. |
| Floe | Secure Boot off + TPM absent + `luks-passphrase` | Same provenance, device, login and configuration checks; verify boot-time passphrase unlock. |
| Sundog | Confirm image/key/catalog first; if secure-capable, Secure Boot on + TPM present + `tpm2-luks` | Same provenance, device, login and configuration checks; if not secure-capable, explicitly reallocate the Secure Boot cell to a supported image. |
| Sundog | Image, key and catalog entry ship in snosi's catalog; if secure-capable, Secure Boot on + TPM present + `tpm2-luks` | Same provenance, device, login and configuration checks; if not secure-capable, explicitly reallocate the Secure Boot cell to a supported image. |
| Snowfield | Decide whether in the post-cutoff catalog/support scope before qualification | If included, assign a supported distinct hardware cell and record the same provenance, ISO, login and configuration evidence; otherwise record exclusion and rationale. |

For each qualified cell, capture the exact published image digest/ref,
Expand All @@ -312,48 +314,49 @@ this phase does not rewrite its historical A/B evidence or mark it done.
decision has been made. **Not complete; implementation in progress.**

<a id="phase-10"></a>
## Phase 10 — Image-published core flatpaks (small, cross-repo) — 📝 proposed
## Phase 10 — Image-published core flatpaks (small, cross-repo) — ⏳ in progress

**Proposed.** [ADR-0018 (Proposed)](../adr/0018-image-published-core-flatpaks-label.md)
**In progress.** [ADR-0018](../adr/0018-image-published-core-flatpaks-label.md)
moves the `core_flatpaks` set from first-setup's `core.json` and the ISO's
`/usr/share/firn/core-flatpaks.json` to an `org.frostyard.core-flatpaks`
label each image publishes. Today every image, including Sundog and Floe,
gets first-setup's GNOME list when the user opts in. Order matters: snosi
labels ship before firn stops reading the old paths.
label each image publishes ([spec](../specs/core-flatpaks-label.md)).
Before it, every image, including Sundog and Floe, got first-setup's GNOME
list when the user opted in. Order matters: snosi labels ship before a firn
release stops reading the old paths.

Out of scope: offline flatpak seeding. Published ISOs carry no seed, and a
locally seeded ISO keeps today's whole-tree copy (ADR-0018 Consequences).
Migrating existing installs and removing Sundog's native apps belong to
snosi.

1. **Settle the label contract (firn docs).** ADR-0018, still Proposed,
defines the parser cases, preflight-time validation, and which image the
set describes.
2. **Snosi: publish the label.** Add `flatpaks/snow.json` and
`flatpaks/sundog.json` (Snowfield uses `snow.json`; Floe has none) and
validate them in CI. Pass the label at every `buildah-package.sh` call
site. Generate first-setup's `core.json` on Snow from `snow.json` with a
drift check. CI checks the label on the final pushed images and its
absence on Floe.
3. **Firn: consume the label.** Accept ADR-0018 and write the label spec
(new `docs/specs/` entry) with this code. `preflight-image` returns the
selected source's labels, parses them when `core_flatpaks = true`, and
keeps the set for `runFlatpaks` in `internal/steps/bootc.go`;
`internal/flatpak` replaces `CoreSet` / `InstallerCoreSet` with the
parser. Update the `core_flatpaks` row in the
[recipe schema](../specs/recipe-schema.md), the
[progress protocol](../specs/progress-protocol.md), and
[architecture](../design/architecture.md) in the same change, and correct
the Phase 9 Sundog note (Sundog is offered through snosi's
`/etc/firn/catalog.json`). Tag only after step 2's images are published.
4. **Firn TUI.** Inspect the chosen image with a timeout and again on image
change; distinguish inspect failure, no core set, malformed label and a
valid set; hiding the toggle clears it. Unit-test navigation and failure
states, and re-sync `test/e2e-tui.sh` per the drive-tui-e2e skill against
a fixture rather than live registry labels.
1. ✅ **Settle the label contract (firn docs).** ADR-0018 defines the parser
cases, preflight-time validation, and which image the set describes
(#110).
2. ✅ **Snosi: publish the label** (frostyard/snosi#1054).
`flatpaks/snow.json` (Snow and Snowfield) and `flatpaks/sundog.json`;
Floe has none. CI validates the files, every packaging lane passes the
label and checks it, and the secure lane checks the pushed digest. The
ISO fallback and seed read `flatpaks/legacy/firn-core-flatpaks.json`,
generated from `snow.json`; first-setup never read its own `core.json`.
3. ✅ **Firn: consume the label.** ADR-0018 accepted; the
[label spec](../specs/core-flatpaks-label.md) lands with the code.
`preflight-image` returns the selected source's labels, parses them when
`core_flatpaks = true` (malformed fails with
`core_flatpaks_label_invalid`), and keeps the set for `runFlatpaks`,
which installs explicit apps then the core set, each once.
`internal/flatpak` no longer reads first-setup's `core.json` or the ISO
fallback. Tag only after step 2's images are published.
4. ✅ **Firn TUI.** The flatpaks page inspects the chosen image with a
timeout and again on image change, and distinguishes inspect failure, no
core set, a malformed label and a valid set; hiding the toggle clears it.
Unit tests cover image switching and every state. `test/e2e-tui.sh`
asserts Floe's real outcome (no toggle, an explanation); the guest
already pulls Floe from GHCR, so this adds no new registry dependency.
5. **Snosi: retire the fallback.** Once the ISO carries a step-3 firn
release, remove `/usr/share/firn/core-flatpaks.json` from
`shared/firn-installer/mkosi.conf` and the Justfile's `_firn-binary`.
release, remove the `/usr/share/firn/core-flatpaks.json` embedding from
`shared/firn-installer/mkosi.conf` and the Justfile's `_firn-binary`,
and point the seed at `flatpaks/snow.json` so
`flatpaks/legacy/firn-core-flatpaks.json` can go.

- **Done when:** Snow and Sundog installs with `core_flatpaks = true` each
land their own image's set, a malformed label fails preflight before any
Expand Down
98 changes: 98 additions & 0 deletions docs/specs/core-flatpaks-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Spec: Core Flatpaks image label (version 1)

This contract governs the `org.frostyard.core-flatpaks` OCI image label: the
core Flatpak set an image publishes for firn to install when a recipe sets
`core_flatpaks = true`. Snosi produces it from `flatpaks/` in every image
build; firn's `preflight-image` step consumes it (`internal/flatpak`
`ParseCoreLabel`, `internal/steps/preflight.go`). It replaces first-setup's
`core.json` and the installer ISO's `/usr/share/firn/core-flatpaks.json` as
the source of the core set ([ADR-0018](../adr/0018-image-published-core-flatpaks-label.md)).

## Interface

The label value is a single JSON object:

```json
{"version":1,"flatpaks":[{"id":"org.mozilla.firefox","name":"Firefox"}]}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `version` | integer | yes | Exactly `1`. |
| `flatpaks` | array of objects | yes | May be empty (no core set). |
| `flatpaks[].id` | string | yes | Flatpak application ID: at least three dot-separated elements of `[A-Za-z0-9_-]`, none starting with a digit, `-` only in the last element, at most 255 characters. |
| `flatpaks[].name` | string | yes | Display name; not empty or whitespace only. |

Field names are exact and case-sensitive. An image with no core set
publishes no label key.

## Rules

1. Firn parses the label fail-closed. Every row below other than "No core
set" and "Accepted" is **malformed**:

| Label content | Result |
| --- | --- |
| Key absent | No core set |
| Empty string | Malformed |
| `null`, or not a single JSON object (invalid JSON, trailing data) | Malformed |
| `version` missing, null, not an integer, or not `1` (`1.0` and `true` are not `1`) | Malformed |
| `flatpaks` missing or null | Malformed |
| Any field other than `version`, `flatpaks`, and per entry `id`, `name`, including case variants such as `VERSION` or `Id` | Malformed |
| An entry that is null, or whose `id` is missing, empty, or not a valid application ID | Malformed |
| `name` missing, empty, or whitespace only | Malformed |
| `flatpaks` is an empty array | No core set |
| Duplicate `id` | Accepted; later duplicates dropped, first occurrence kept |

2. Firn reads the label in `preflight-image`, before any disk write, from
the image that step selects: the local containers-storage copy when it is
selected, otherwise the registry image. For a signed recipe that is the
cosign-verified digest firn deploys. For an unsigned recipe bootc resolves
the tag again when it pulls, so a tag that moves during the install can
deploy a newer build than the label described
([firn#109](https://github.com/frostyard/firn/issues/109)).
3. With `core_flatpaks = true`:
- a malformed label fails `preflight-image` with error code
`core_flatpaks_label_invalid`, in dry-run and real runs alike;
- labels that could not be read fail `preflight-image` too. A signed
`@sha256:` reference can verify while its registry inspection failed
and no matching local copy exists; its labels are then unknown, not
absent, and the install stops rather than report `no_core_set`;
- no core set emits the `no_core_set` warning and summary item, and the
install continues;
- otherwise the set's IDs are installed after the recipe's explicit
`flatpaks`, each ID once at its first occurrence, through ADR-0006's
offline-first provisioning.
4. With `core_flatpaks = false`, firn does not parse the label; a malformed
label cannot affect the install.
5. A new format increments `version`. Firn releases that do not know a
version treat its label as malformed.
6. Producers must not publish an empty `flatpaks` array or an empty-string
value; an image without a core set carries no label key.
7. The TUI wizard previews the label on its flatpaks page, inspecting the
chosen image with rule 2's local-first selection (no signature
verification) under a 30-second timeout, and again when the chosen image
changes. It:
- lists a valid set's names beside the `core_flatpaks` toggle;
- shows no toggle, and an explanation, for no core set or a malformed
label, and sets `core_flatpaks` to false;
- keeps the toggle, noting the list is read at install time, when
inspection fails or returns no readable metadata, and inspects again on
the next visit.

The preview is display-only and never written into the recipe.

## Derived artifacts

| Artifact | Derivation |
| --- | --- |
| Snosi `flatpaks/*.json` and `flatpaks/core-flatpaks.py` | Produce the label per product and check it on every packaged and pushed image; snosi's `test/core-flatpaks-test.sh` mirrors rule 1's rejections. |
| Progress codes `no_core_set`, `core_flatpaks_label_invalid` | [Progress protocol](progress-protocol.md#stable-codes). |
| `core_flatpaks` recipe field | [Recipe schema](recipe-schema.md). |

## References

- Rationale: [ADR-0018](../adr/0018-image-published-core-flatpaks-label.md),
[ADR-0006](../adr/0006-install-time-offline-first-flatpaks.md)
- Context: [design/architecture.md](../design/architecture.md) (system
configuration, flatpaks)
Loading
Loading