docs(app-platform): sync the platform docs with the shipped code

- app-manifest-spec.md: full rewrite from the real schema in
  core/container/src/manifest.rs — it was 5 months stale and missing the
  entire modern feature set (build, network/network_aliases, derived_env,
  secret_env, generated_secrets/certs, data_uid, files, interfaces, hooks,
  extensions flatten) and documented wrong network_policy values.
- app-developer-guide.md: add generated_secrets/generated_certs/
  network_aliases/hooks to the field table.
- APP-PACKAGING-MIGRATION-PLAN.md: phase status stamped (1-3 done, 5 mostly,
  4+6 open); deleted meshtastic app removed from regression-proof lists.
- registry-manifest-design.md: status design → implemented (phases 1-3),
  stale manifest_dir:Option line fixed.
- marketplace-protocol.md: reframed proposal → as-built (marketplace.rs +
  RPCs + UI shipped; create-invoice noted; schema disambiguated).
- manifest-hooks-design.md: phase 3 (indeedhub) done, phase 4 resolved via
  orchestrator+generated_secrets instead of hooks.
- README/architecture: restore the NIP-07 signer-bridge claim — it is real
  (neode-ui/public/nostr-provider.js), the earlier audit only grepped Rust.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-08 17:24:22 -04:00
co-authored by Claude Fable 5
parent 9897af1236
commit 6e2d128c51
8 changed files with 191 additions and 128 deletions
+135 -106
View File
@@ -1,126 +1,155 @@
# App Manifest Specification
## Overview
_Accurate as of 2026-07-08. The canonical schema is the Rust parser in
`core/container/src/manifest.rs` (`AppManifest``AppDefinition`); if this
document and the code disagree, the code wins. See
[`app-developer-guide.md`](app-developer-guide.md) for the authoring workflow._
App manifests define containerized applications in Archipelago. They use YAML format and specify container configuration, dependencies, resources, security policies, and integration metadata.
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.
## File Location
## Top-level fields (`app:`)
App manifests are stored in `apps/{app-id}/manifest.yml`
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `id` | string | ✅ | Lowercase alphanumeric + `-`/`_`. Must match the directory name. |
| `name` | string | ✅ | Display name. |
| `version` | string | ✅ | App version shown in the UI. |
| `description` | string | — | One-line description. |
| `container` | ContainerConfig | — | Image/build source + runtime shape (below). |
| `dependencies` | list | — | `- storage: "10GB"`, `- { app_id: bitcoin, version: … }`, or a bare string. |
| `resources` | ResourceLimits | — | `cpu_limit` (int), `memory_limit` (e.g. `"512m"`), `disk_limit`. |
| `security` | SecurityPolicy | — | See [Security](#security). |
| `ports` | list of PortMapping | — | `- { host: 8080, container: 80, protocol: tcp }`. |
| `volumes` | list of Volume | — | See [Volumes](#volumes). |
| `files` | list of GeneratedFile | — | Config files written before create: `{ path, content, overwrite }`. `path` must sit under a declared bind mount. |
| `environment` | list of string | — | `- KEY=value` pairs (static). |
| `health_check` | HealthCheck | — | `{ type, endpoint/path, interval, timeout, retries }`. `type` is free-form today; `http` is what the monitor exercises. |
| `devices` | list of string | — | Host device paths; must start with `/dev/`. |
| `interfaces` | map | — | Launch surfaces, keyed by name (`main`): `{ name, description, type, port, protocol, path }`. |
| `hooks` | LifecycleHooks | — | Allow-listed lifecycle hooks. See [Hooks](#hooks). |
| _anything else_ | — | — | Unknown keys are absorbed into an `extensions` map (serde flatten) and treated as transitional metadata — e.g. `container_name`, `metadata`, `category`, `bitcoin_integration`, `lightning_integration`. These are **not** typed schema; do not rely on them being validated. |
## Schema
## `container:` (ContainerConfig)
### Required Fields
Exactly **one** of `image` or `build` must be present (image XOR build).
| Field | Type | Notes |
|-------|------|-------|
| `image` | string | Registry reference. Pull source. |
| `image_signature` | string | Optional signature reference for image verification. |
| `pull_policy` | string | Default `if-not-present`. |
| `build` | BuildConfig | Local build: `{ context, dockerfile (default "Dockerfile"), tag, build_args }`. |
| `network` | string | Literal podman `--network` value (`archy-net`, `host`, a stack network, …). Omitted = rootless default isolated network. |
| `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. |
| `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. |
| `data_uid` | string | `"UID:GID"` applied to the app's bind-mounted data dir before create (rootless subuid mapping, e.g. Postgres). |
## Security
```yaml
app:
id: string # Unique app identifier (lowercase, kebab-case)
name: string # Human-readable name
version: string # Semantic version (e.g., "1.0.0")
container:
image: string # Container image (e.g., "bitcoin/bitcoin:26.0")
security:
readonly_root: true # default true
no_new_privileges: true # default true
capabilities: [CHOWN] # default [] (cap-drop ALL, add back only these)
network_policy: isolated # isolated | bridge | host (default isolated)
apparmor_profile: null # optional profile name
```
### Optional Fields
Validation (enforced at `AppManifest::validate()`):
- Capabilities must come from the reviewed allow-list (CHOWN, DAC_OVERRIDE,
FOWNER, NET_ADMIN, NET_BIND_SERVICE, NET_RAW, SETGID, SETUID, SYS_ADMIN).
- `network_policy` must be exactly `isolated`, `bridge`, or `host`.
- No `container:`/`ns:` network modes; devices must be `/dev/*`.
- Bind-mount sources are confined to `/var/lib/archipelago` (reviewed
exceptions: the rootless podman socket and dbus).
- `derived_env` templates may only use the placeholder allow-list;
`secret_env`/`generated_secrets` names must be bare filenames.
- Hook steps are validated against the hook allow-list (below).
## Volumes
```yaml
volumes:
- type: bind # bind | volume | tmpfs
source: /var/lib/archipelago/myapp/data
target: /data
options: [rw] # allow-list: rw, ro, z, Z, shared, …
- type: tmpfs
target: /tmp
tmpfs_options: "rw,noexec,nosuid,size=256m"
```
## Hooks
Declarative, allow-listed operations that run against the app's **own
container** — never the host (design: `manifest-hooks-design.md`).
```yaml
hooks:
post_install: # runs once after install, container running
- copy_from_host: # src relative to an allow-listed root (data dir / web-ui);
src: web-ui/nostr-provider.js # no absolute paths, no '..'
dest: /usr/share/nginx/html/nostr-provider.js
- exec: ["sh", "-c", "nginx -s reload"] # podman exec inside the container
pre_start: [] # reserved in the schema; executor not yet wired
```
## 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.
## Distribution
Manifests ship two ways:
1. **Signed catalog** (primary): `releases/app-catalog.json` embeds the full
manifest per app and carries an Ed25519 detached signature verified against
the pinned release-root anchor. Nodes overlay catalog manifests over disk
files — **catalog wins** for image-only apps; `apps/<id>/manifest.yml` on
disk remains the fallback and is still required for build-source apps.
2. **Decentralized marketplace**: Nostr NIP-78 discovery with DID-signed
manifests ([`marketplace-protocol.md`](marketplace-protocol.md)). Note the
marketplace uses its own flatter manifest schema, not this one.
## Minimal example
```yaml
app:
description: string # App description
id: myapp
name: My App
version: 1.0.0
description: Does something useful
container:
image_signature: string # Cosign signature URL (e.g., "cosign://...")
pull_policy: string # "if-not-present" | "always" | "never"
dependencies:
- storage: string # Minimum disk space (e.g., "500Gi")
- app_id: string # Required app dependency
version: string # Version constraint (e.g., ">=26.0")
- string # Simple app dependency
resources:
cpu_limit: number # CPU cores (e.g., 2)
memory_limit: string # Memory limit (e.g., "2Gi", "512Mi")
disk_limit: string # Disk limit (e.g., "500Gi")
security:
capabilities: [string] # Linux capabilities (e.g., ["NET_BIND_SERVICE"])
readonly_root: boolean # Read-only root filesystem (default: true)
network_policy: string # "isolated" | "host" | network name
apparmor_profile: string # AppArmor profile name
image: docker.io/vendor/myapp:1.0.0
generated_secrets:
- { name: myapp-admin-password, kind: hex16 }
secret_env:
- { key: ADMIN_PASSWORD, secret_file: myapp-admin-password }
ports:
- host: number # Host port
container: number # Container port
protocol: string # "tcp" | "udp" (default: "tcp")
- { host: 8090, container: 8080 }
volumes:
- type: string # "bind" | "tmpfs" | "volume"
source: string # Host path
target: string # Container path
options: [string] # Mount options (e.g., ["rw", "noexec"])
environment:
- string # Environment variable (e.g., "NETWORK=mainnet")
devices:
- string # Device path (e.g., "/dev/ttyUSB0")
- { type: bind, source: /var/lib/archipelago/myapp, target: /data, options: [rw] }
health_check:
type: string # "http" | "exec"
endpoint: string # HTTP URL or command
path: string # HTTP path (for http type)
interval: string # Check interval (e.g., "30s")
timeout: string # Timeout (e.g., "5s")
retries: number # Failure retries (default: 3)
# Integration-specific metadata
bitcoin_integration:
rpc_access: string # "admin" | "read-only"
sync_required: boolean # Requires synced node
testnet_support: boolean
pruning_support: boolean
lightning_integration:
channel_management: boolean
payment_routing: boolean
nostr_integration:
relay_type: string # "public" | "private"
monetization_enabled: boolean
event_storage: string # "sqlite" | "postgres"
web5_integration:
did_support: boolean
dwn_protocol: boolean
sync_enabled: boolean
networking:
mesh_enabled: boolean
local_network_access: boolean
device_discovery: boolean
routing_protocols: [string] # e.g., ["olsr", "babel"]
type: http
path: /health
interfaces:
main:
type: web
port: 8090
```
## Examples
See `apps/` directory for complete examples:
- `apps/bitcoin-core/manifest.yml`
- `apps/lnd/manifest.yml`
- `apps/nostr-rs-relay/manifest.yml`
- `apps/meshtastic/manifest.yml`
## Validation
Manifests are validated on installation:
- Required fields present
- Version format valid
- Resource limits reasonable
- Port conflicts detected
- Dependency cycles prevented
## Versioning
- Use semantic versioning (MAJOR.MINOR.PATCH)
- Breaking changes increment MAJOR
- New features increment MINOR
- Bug fixes increment PATCH
Validate with `scripts/validate-app-manifest.sh` and regenerate the catalog
with `scripts/generate-app-catalog.py` (drift-checked in CI by
`scripts/check-app-catalog-drift.py`).