archy/docs/app-developer-guide.md

340 lines
12 KiB
Markdown
Raw Normal View History

# Archipelago App Developer Guide
Build and package containerized apps for Archipelago.
## Overview
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 at `apps/<app-id>/manifest.yml`. The root key is `app`; runtime, catalog, and integration fields live below that key.
### Template Manifest
```yaml
# apps/my-app/manifest.yml
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
ports:
- host: 8180
container: 8080
protocol: tcp
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
| Field | Description |
|-------|-------------|
| `app.id` | Unique identifier, lowercase, kebab-case only |
| `app.name` | Human-readable name |
| `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/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**`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
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 |
| `DAC_OVERRIDE` | App needs to bypass file permissions |
| `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
- Namespace-sharing network modes such as `container:<name>` or `ns:<path>`
- Mounting system paths: `/`, `/etc`, `/var`, `/usr`, `/proc`, `/sys`
- `SYS_PTRACE`, privileged containers, Docker socket mounts, or rootful execution
- Hardcoded secrets in environment variables or images
## Container Best Practices
### Volumes
```yaml
volumes:
- 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:
```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
```
### Logging
- Log to stdout/stderr (Podman captures container logs)
- Never log secrets, passwords, or keys
- Use structured logging (JSON) for machine parsing
### Networking
Apps get their own network namespace. To connect to other Archipelago apps:
```yaml
# If your app needs to talk to Bitcoin
dependencies:
- bitcoin-knots
container:
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. Use `derived_env` for host facts like `HOST_MDNS` instead of hardcoding node-specific URLs.
## 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
```bash
podman build -t docker.io/myorg/my-app:1.0.0 .
podman push docker.io/myorg/my-app:1.0.0
```
### 2. Generate Catalogs
```bash
python3 scripts/generate-app-catalog.py
```
### 3. Verify Drift
```bash
python3 scripts/check-app-catalog-drift.py --release --strict
```
Before release, the canonical catalog and UI public catalog should match:
```bash
cmp -s app-catalog/catalog.json neode-ui/public/catalog.json
```
## Testing Your App
### Local Testing
```bash
# Run your container locally
podman run -d --name my-app \
-p 8180:8080 \
--read-only \
--security-opt no-new-privileges \
--user 1000:1000 \
docker.io/myorg/my-app:1.0.0
# Verify it works
curl http://localhost:8180/health
# Check logs
podman logs my-app
```
### On an Archipelago Node
1. Install via the marketplace UI or RPC:
```bash
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
-d '{"method":"package.install","params":{"id":"my-app","dockerImage":"docker.io/myorg/my-app:1.0.0"}}'
```
2. Verify the container is running:
```bash
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
-d '{"method":"container-list"}'
```
3. Check the UI at `http://192.168.1.228/app/my-app/`
### Validate Manifest
```bash
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. 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
- Provide a URL to your app icon (PNG, WebP, or SVG)
- 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.