docs: capture 1.8 app migration release plan
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user