docs(app-manifest-spec): fix the id rule, the Quadlet claim, and the declarative overstatement

Narrative pass over the manifest spec, plus one correction to the guide I
committed in ba052736.

- **`app.id` allowed `_`.** `is_valid_app_id` accepts lowercase ASCII letters,
  digits and single hyphens only — no underscores, no leading/trailing hyphen,
  no `--`. The spec's "alphanumeric + `-`/`_`" would have a developer write an id
  that fails to parse.
- **"Must match the directory name"** is a convention, not a rule. The loader
  (`prod_orchestrator.rs:1455-1474`) walks `*/manifest.yml` and keys off
  `app.id`, never comparing it to the folder, so a mismatch silently registers
  the app under a different id. Said so rather than implying enforcement.
- **The Quadlet claim was the same one architecture.md was corrected for**
  (f55ed6bf): install does NOT compile to a `user.slice` Quadlet unit today.
  `config.use_quadlet_backends` defaults false, so apps take the legacy
  `podman create + start` path; Quadlet is opt-in per node and companion UIs are
  the exception that already use it.
- **"no per-app installer code"** — true of installers, but
  `run_pre_start_hooks` is a hardcoded `match app_id` covering seven first-party
  apps (bitcoin-ui, filebrowser, lnd, archy-nbxplorer, btcpay-server,
  fedimint-clientd, grafana). Documented as the caveat it is; anyone reading the
  source will find it in a minute and the doc should not look like it's hiding it.
- `derived_env` now names the full closed allow-list including `{{BITCOIN_HOST}}`
  and what it resolves to.

Correction to ba052736: I wrote there that an unknown `derived_env` placeholder
passes through verbatim. It doesn't — `validate_derived_template` rejects both
unknown names and unbalanced `{{`. Fixed that row in the guide.

Verified accurate and left alone: the capability allow-list, network_policy
values, `/dev/*` device rule, volume option allow-list, bind-source confinement,
the four generated_secret kinds, `hooks.pre_start` being schema-only, and the
30s reconciler interval (`BootReconciler::DEFAULT_INTERVAL`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 20:49:17 -04:00
co-authored by Claude Opus 5
parent ba052736be
commit 7adc3260a6
2 changed files with 25 additions and 10 deletions
+1 -1
View File
@@ -109,7 +109,7 @@ app:
| `app.container.pull_policy` | Pull behavior, usually `if-not-present` |
| `app.container.network` | Podman network setting such as `archy-net` or `pasta`; dangerous namespace-sharing modes are rejected |
| `app.container.entrypoint` / `custom_args` | Entrypoint and command override |
| `app.container.derived_env` | Environment values rendered from host facts. The complete placeholder set is `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}` anything else is left in the value verbatim rather than erroring, so a typo ships a literal `{{FOO}}` to your container |
| `app.container.derived_env` | Environment values rendered from host facts. The complete placeholder set is `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}`; an unknown name or an unbalanced `{{` is a parse error, so typos fail loudly |
| `app.container.secret_env` | Environment values read from `/var/lib/archipelago/secrets/<secret_file>`, injected as podman secrets (never visible in `podman inspect` or unit files) |
| `app.container.generated_secrets` | Secrets the orchestrator creates on first use (`hex16`/`hex32`/`base64`/`bcrypt`) — self-healing, 0600, no host provisioning |
| `app.container.generated_certs` | Self-signed TLS certs materialised before create; CN/SANs rendered from host facts |
+24 -9
View File
@@ -6,14 +6,22 @@ document and the code disagree, the code wins. See
[`app-developer-guide.md`](app-developer-guide.md) for the authoring workflow._
Every app is a directory `apps/<id>/` containing a `manifest.yml` with a single
top-level `app:` block. Apps are purely declarative — the orchestrator owns the
entire lifecycle; there is no per-app installer code.
top-level `app:` block. Apps are declarative — the orchestrator owns the entire
lifecycle; there is no per-app installer code.
One honest caveat: seven first-party apps still get Rust-side pre-start work
through a hardcoded `match app_id` in `ProdOrchestrator::run_pre_start_hooks`
`bitcoin-ui`, `filebrowser`, `lnd`, `archy-nbxplorer`, `btcpay-server`,
`fedimint-clientd` and `grafana` render or repair a config before start. That is
orchestrator code rather than a per-app installer, but it is not
manifest-declared, and the direction of travel is to replace each case with a
reusable manifest primitive.
## Top-level fields (`app:`)
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `id` | string | ✅ | Lowercase alphanumeric + `-`/`_`. Must match the directory name. |
| `id` | string | ✅ | Lowercase ASCII letters, digits and single hyphens only — **no underscores**, no leading/trailing `-`, no `--` (`is_valid_app_id`). Should match the directory name, though nothing enforces that: the loader keys off `app.id`, so a mismatch silently registers the app under the id in the file rather than the folder. |
| `name` | string | ✅ | Display name. |
| `version` | string | ✅ | App version shown in the UI. |
| `description` | string | — | One-line description. |
@@ -45,7 +53,7 @@ Exactly **one** of `image` or `build` must be present (image XOR build).
| `network_aliases` | list of string | Extra DNS names on `network` (podman `--network-alias`) — lets stack members answer to short baked-in hostnames (`api`, `minio`, `relay`). |
| `entrypoint` | list of string | Entrypoint override. |
| `custom_args` | list of string | Extra positional args appended after the image. |
| `derived_env` | list | `- { key, template }` — template rendered against host facts at apply time. Allowed placeholders: `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}` (plus dependency-resolved facts such as the active bitcoin host). Never hard-code host specifics. |
| `derived_env` | list | `- { key, template }` — template rendered against host facts at apply time. The allow-list is exactly `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}` (`DERIVED_PLACEHOLDERS`); an unknown name or unbalanced `{{` fails validation. `{{BITCOIN_HOST}}` resolves to whichever Bitcoin app is running (`bitcoin-knots` or `bitcoin-core`, defaulting to knots). Never hard-code host specifics. |
| `secret_env` | list | `- { key, secret_file }` — value read from `/var/lib/archipelago/secrets/<secret_file>` and injected as a **podman secret**, so it never appears in `podman inspect` or unit files. `secret_file` must be a bare filename (no `/`, no `..`). |
| `generated_secrets` | list | `- { name, kind }` — orchestrator materialises the secret on first use (0600, rootless service user, idempotent + self-healing). `kind ∈ hex16 | hex32 | base64 | bcrypt` (bcrypt writes `<name>` = hash and `<name>.pw` = plaintext). |
| `generated_certs` | list | `- { crt, key, common_name?, sans? }` — self-signed TLS materialised before create; CN/SANs rendered against host facts. |
@@ -104,11 +112,18 @@ hooks:
## Installation semantics
The orchestrator compiles the manifest into a rootless Podman **Quadlet unit
under `user.slice`** — the container survives backend restarts and reboots, and
a level-triggered reconciler converges drift every 30 seconds. Multi-container
apps are sets of per-member manifests installed together via the stack
orchestrator (`api/rpc/package/stacks.rs`) on an app-local network.
The orchestrator compiles the manifest into a rootless Podman container that
survives backend restarts and reboots, and a level-triggered reconciler
converges drift every 30 seconds (`BootReconciler::DEFAULT_INTERVAL`).
Multi-container apps are sets of per-member manifests installed together via
the stack orchestrator (`api/rpc/package/stacks.rs`) on an app-local network.
**Quadlet is not the default path.** `config.use_quadlet_backends` defaults to
`false`, so ordinary apps still take the legacy `podman create + start` path;
the Quadlet-unit-under-`user.slice` backend is opt-in per node (config key or
`ARCHIPELAGO_USE_QUADLET_BACKENDS`) and stays behind the flag until the
lifecycle harness has gone green against it. Companion UI containers are the
exception that do use Quadlet today.
## Distribution