docs: capture 1.8 app migration release plan

This commit is contained in:
archipelago
2026-06-11 00:24:54 -04:00
parent 1a3d726eac
commit 182f18ecf3
10 changed files with 3392 additions and 205 deletions

View File

@@ -1,14 +1,18 @@
# Archipelago App Developer Guide
Build and publish containerized apps for the Archipelago ecosystem.
Build and package containerized apps for Archipelago.
## Overview
Apps run as Podman containers on user nodes. You publish app manifests to Nostr relays, where nodes discover and install them through the community marketplace.
Apps run as rootless Podman containers on user nodes. You describe an app in `apps/<app-id>/manifest.yml`; the backend validates that manifest, compiles it into rootless container/runtime behavior, and the release pipeline generates catalog surfaces from the same manifest-owned metadata.
Archipelago's app contract is deliberately manifest-first. A developer should be able to describe images or local builds, ports, volumes, generated files, dependencies, health/readiness, data ownership, networking, secrets, and supported bridge integrations in the app manifest without asking for a custom OS image or app-specific backend patch. When a real app needs a capability that is not represented yet, the preferred path is to add a reusable manifest/orchestrator primitive that other apps can use too.
The historical marketplace-publish design is not the active local developer contract for `1.8-alpha`. For this release, local manifests are the source of truth and catalog JSON is generated from them.
## App Manifest
Every app needs a manifest (YAML for local apps, JSON for marketplace publishing).
Every app needs a manifest at `apps/<app-id>/manifest.yml`. The root key is `app`; runtime, catalog, and integration fields live below that key.
### Template Manifest
@@ -18,36 +22,70 @@ app:
id: my-app # Unique, lowercase kebab-case
name: My App
version: 1.0.0 # Semantic versioning
description: My App does one thing well.
container:
image: docker.io/myorg/my-app:1.0.0
pull_policy: if-not-present
network: archy-net
entrypoint: ["sh", "-lc"]
custom_args:
- /app/start.sh
derived_env:
- key: PUBLIC_URL
template: https://{{HOST_MDNS}}:8180
secret_env:
- key: APP_PASSWORD
secret_file: my-app-password
dependencies:
- storage: 1Gi
resources:
cpu_limit: 2
memory_limit: 512Mi
security:
capabilities: []
readonly_root: true
no_new_privileges: true
network_policy: isolated
container:
image: docker.io/myorg/my-app:1.0.0 # Never use :latest
ports:
- container: 8080
host: 8180
- host: 8180
container: 8080
protocol: tcp
volumes:
- name: data
path: /data
env:
APP_MODE: production
capabilities: [] # Only add if absolutely necessary
readonly_root: true # Required
no_new_privileges: true # Required
run_as_user: 1000 # Must be >= 1000
metadata:
description:
short: "One-line description (max 120 chars)"
long: "Detailed description of what this app does and why."
author:
name: "Your Name"
did: "did:key:z6Mk..." # Your Archipelago node DID
category: money # money | commerce | data | networking | home | community | other
icon_url: "https://example.com/icon.png"
repo_url: "https://github.com/myorg/my-app"
license: MIT
min_archipelago_version: "0.1.0"
dependencies: [] # e.g., ["bitcoin-knots"] if this app needs Bitcoin
volumes:
- type: bind
source: /var/lib/archipelago/my-app
target: /data
options: [rw]
environment:
- APP_MODE=production
health_check:
type: http
endpoint: http://localhost:8080
path: /health
interval: 30s
timeout: 5s
retries: 3
files:
- path: /var/lib/archipelago/my-app/config.yml
content: |
bind: 0.0.0.0:8080
overwrite: false
metadata:
icon: /assets/img/app-icons/my-app.svg
category: tools
tier: optional
repo: https://github.com/myorg/my-app
launch:
open_in_new_tab: false
```
### Required Fields
@@ -56,39 +94,71 @@ metadata:
|-------|-------------|
| `app.id` | Unique identifier, lowercase, kebab-case only |
| `app.name` | Human-readable name |
| `app.version` | Semantic version (major.minor.patch) |
| `container.image` | Full image reference with pinned version tag |
| `metadata.description.short` | One-line description, max 120 characters |
| `metadata.author.did` | Your node's DID (get via `node.did` RPC) |
| `app.version` | Version string containing at least one digit; semantic versions are preferred |
| `container.image` or `container.build` | Exactly one image source must be present |
| `security.readonly_root` | Should remain `true` for normal apps |
| `security.no_new_privileges` | Should remain `true` for normal apps |
### Current Manifest Fields
| Field | Purpose |
|-------|---------|
| `app.id`, `app.name`, `app.version`, `app.description` | App identity and release metadata |
| `app.container.image` | Registry image to pull |
| `app.container.build` | Local build definition with `context`, `dockerfile`, `tag`, and optional `build_args` |
| `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 allowed host facts such as `HOST_IP`, `HOST_MDNS`, and `DISK_GB` |
| `app.container.secret_env` | Environment values read from `/var/lib/archipelago/secrets/<secret_file>` |
| `app.container.data_uid` | UID:GID ownership repair for app data directories |
| `app.dependencies` | Storage requirements and app dependencies |
| `app.resources` | CPU, memory, and disk limits |
| `app.security` | Capabilities, read-only root, no-new-privileges, network policy, optional AppArmor profile |
| `app.ports` | Host-to-container port mappings |
| `app.volumes` | `bind`, `volume`, or `tmpfs` mounts |
| `app.files` | Generated files under declared bind-mounted host paths |
| `app.environment` | Static `KEY=value` environment entries |
| `app.health_check` | HTTP or TCP health check settings |
| `app.devices` | Explicit device paths |
| `app.metadata` | Catalog-facing presentation metadata such as icon, category, tier, repo/source, author, feature bullets, and launch hints |
Additional extension keys may exist for current integrations, for example Bitcoin, Lightning, or app-specific launch/interface metadata. Treat extension keys as transitional unless they are documented as reusable platform primitives.
Use `metadata.launch.open_in_new_tab: true` when the app UI is known to reject iframe embedding with headers such as `X-Frame-Options` or restrictive CSP. The frontend app-session metadata is generated from this flag during release work.
## Security Requirements
These are enforced by the marketplace and the node. Non-compliant apps are flagged.
These are enforced by the marketplace/catalog pipeline and the node. Non-compliant apps are flagged.
### Mandatory
1. **No `:latest` tag** — Pin a specific version: `myapp:1.0.0`
2. **Read-only root filesystem**`readonly_root: true` (use volumes for writable data)
3. **Non-root user**`run_as_user: 1000` or higher
4. **No privilege escalation**`no_new_privileges: true`
5. **Minimal capabilities** — Drop all caps, only add required ones
2. **Read-only root filesystem**`security.readonly_root: true` (use volumes for writable data)
3. **No privilege escalation**`security.no_new_privileges: true`
4. **Minimal capabilities** — Drop all caps, only add required ones
5. **No host network unless explicitly approved** — keep `security.network_policy` isolated or bridge
### Allowed Capabilities
Only these Linux capabilities may be requested:
The parser currently accepts this allow-list. Keep capability requests minimal; some accepted capabilities still require release review before a public package should depend on them.
| Capability | When Needed |
|-----------|-------------|
| `CHOWN` | App needs to change file ownership |
| `NET_BIND_SERVICE` | App binds to ports below 1024 |
| `DAC_OVERRIDE` | App needs to bypass file permissions |
| `SETUID`, `SETGID` | App manages user switching (e.g., nginx) |
| `FOWNER` | App needs ownership-related file operations |
| `NET_ADMIN` | Network administration; requires extra scrutiny |
| `NET_BIND_SERVICE` | App binds to ports below 1024 |
| `NET_RAW` | Raw network sockets; requires extra scrutiny |
| `SETUID`, `SETGID` | App manages user switching |
| `SYS_ADMIN` | Broad administrative capability; avoid for normal apps |
### Forbidden
- `--network host` — Apps cannot share the host network
- Namespace-sharing network modes such as `container:<name>` or `ns:<path>`
- Mounting system paths: `/`, `/etc`, `/var`, `/usr`, `/proc`, `/sys`
- `SYS_ADMIN`, `SYS_PTRACE`, or any privileged capability
- `SYS_PTRACE`, privileged containers, Docker socket mounts, or rootful execution
- Hardcoded secrets in environment variables or images
## Container Best Practices
@@ -97,14 +167,26 @@ Only these Linux capabilities may be requested:
```yaml
volumes:
- name: data # App data persists across restarts
path: /data
- name: config # Configuration files
path: /config
- type: bind
source: /var/lib/archipelago/my-app
target: /data
options: [rw]
```
Data is stored at `/var/lib/archipelago/{app-id}/` on the host.
Generated files must live under a declared bind-mounted host path:
```yaml
files:
- path: /var/lib/archipelago/my-app/config.yml
content: |
bind: 0.0.0.0:8080
overwrite: false
```
Use `overwrite: false` for first-run defaults that users or the app may later modify. Use `overwrite: true` only for generated files the platform must own.
### Health Checks
Define a health check endpoint in your container:
@@ -130,14 +212,30 @@ dependencies:
- bitcoin-knots
container:
env:
BITCOIN_RPC_HOST: bitcoin-knots # Container DNS name on archy-net
BITCOIN_RPC_PORT: "8332"
network: archy-net
derived_env:
- key: BITCOIN_RPC_HOST
template: bitcoin-knots
- key: BITCOIN_RPC_PORT
template: "8332"
```
The `archy-net` Podman network provides DNS resolution between containers.
The `archy-net` Podman network provides DNS resolution between containers. Use `derived_env` for host facts like `HOST_MDNS` instead of hardcoding node-specific URLs.
## Publishing to the Marketplace
## Catalog Generation
Catalog JSON is generated from manifests during release work. Do not manually edit generated fields in `app-catalog/catalog.json` or `neode-ui/public/catalog.json` when the same value belongs in the manifest.
Manifest-owned catalog fields currently include:
- app title from `app.name`;
- version from `app.version`;
- description from `app.description`;
- Docker image from `app.container.image`;
- category from `app.category` or `app.metadata.category`;
- tier from `app.metadata.tier`;
- icon from `app.metadata.icon`;
- repo URL from `app.metadata.repo`, `repoUrl`, or `source`.
### 1. Build and Push Your Image
@@ -146,79 +244,24 @@ podman build -t docker.io/myorg/my-app:1.0.0 .
podman push docker.io/myorg/my-app:1.0.0
```
### 2. Get Your Node's DID
### 2. Generate Catalogs
```bash
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
-d '{"method":"node.did"}'
# Returns: {"result":{"did":"did:key:z6Mk..."}}
python3 scripts/generate-app-catalog.py
```
### 3. Publish via RPC
### 3. Verify Drift
```bash
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
-H "Content-Type: application/json" \
-d '{
"method": "marketplace.publish",
"params": {
"app_id": "my-app",
"name": "My App",
"version": "1.0.0",
"description": {"short": "A useful tool", "long": "Detailed description..."},
"author": {"name": "Dev Name", "did": "did:key:z6Mk...", "nostr_pubkey": ""},
"container": {
"image": "docker.io/myorg/my-app:1.0.0",
"ports": [{"container": 8080, "host": 8180, "protocol": "tcp"}],
"volumes": [],
"env": {},
"capabilities": [],
"readonly_root": true,
"no_new_privileges": true,
"run_as_user": 1000
},
"category": "other",
"icon_url": "",
"repo_url": "https://github.com/myorg/my-app",
"license": "MIT",
"min_archipelago_version": "0.1.0",
"dependencies": []
}
}'
python3 scripts/check-app-catalog-drift.py --release --strict
```
The manifest is published to all configured Nostr relays as a NIP-78 event (kind 30078).
### 4. Verify Discovery
Before release, the canonical catalog and UI public catalog should match:
```bash
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
-d '{"method":"marketplace.discover"}'
# Your app should appear in the results
cmp -s app-catalog/catalog.json neode-ui/public/catalog.json
```
## Trust Model
Published apps receive trust scores (0-100) based on:
| Factor | Points | How to Maximize |
|--------|--------|-----------------|
| Valid DID in author | 30 | Always include your node's DID |
| Found on multiple relays | 5-20 | Configure many relays in your node |
| Developer in federation | 20 | Have federated peers who trust you |
| Proper semver version | 10 | Use `major.minor.patch` format |
| Repository URL present | 5 | Include your repo URL |
| Security compliance | 15 | Meet all security requirements |
### Trust Tiers
| Score | Tier | User Experience |
|-------|------|----------------|
| 80-100 | Verified | One-click install |
| 50-79 | Community | Install with confirmation |
| 20-49 | Unverified | Install with warning |
| 0-19 | Untrusted | Requires explicit override |
## Testing Your App
### Local Testing
@@ -256,18 +299,18 @@ podman logs my-app
### Validate Manifest
```bash
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
-H "Content-Type: application/json" \
-d '{"method":"marketplace.verify","params":{...your manifest...}}'
# Returns: {"result":{"valid":true,"issues":[],"trust_score":65,"trust_tier":"community"}}
cargo test --manifest-path core/Cargo.toml -p archipelago-container
python3 scripts/check-app-catalog-drift.py --release --strict
```
## Updating Your App
1. Build and push the new version: `docker.io/myorg/my-app:1.1.0`
2. Publish an updated manifest with the new version
3. NIP-33 replaceable events: the latest publish overwrites the previous one on relays
4. Nodes running your app can see the update in their marketplace
1. Build and push the new version: `docker.io/myorg/my-app:1.1.0`.
2. Update `app.version` and `app.container.image` or `app.container.build.tag`.
3. Run catalog generation and drift checks.
4. Validate install/start/stop/restart/uninstall/reinstall behavior before shipping.
The broader app update policy for `1.8-alpha` is still being finalized. Until that policy is locked, app manifests should be explicit and pinned so update detection compares concrete image/tag metadata rather than mutable tags.
## App Icon
@@ -275,3 +318,22 @@ curl -b cookies.txt -X POST http://localhost/rpc/v1 \
- Recommended size: 256x256 pixels
- Square aspect ratio
- If no icon URL, a generic placeholder is shown in the marketplace
## Release Validation Expectations
Every supported app must satisfy the lifecycle contract:
- install
- launch
- stop
- start
- restart
- uninstall while preserving data
- reinstall with preserved data
- report truthful health/status
- survive backend restart
- survive host reboot
For apps with special dependencies, launch must explain dependency wait states instead of showing a dead iframe. Examples include Bitcoin sync/IBD, Lightning wallet readiness, Nostr signer bridge injection, Tailscale login/auth, and app-specific setup screens.
Runtime changes should be validated with focused tests first, then the release lifecycle harness on the validation host when host access is intentionally resumed.