5.9 KiB
5.9 KiB
Constraints (from SPECs)
Extracted from 1 classified SPEC: docs/app-manifest-spec.md (accurate as of 2026-07-08). The SPEC self-declares that the canonical schema is the Rust parser in core/container/src/manifest.rs — if doc and code disagree, the code wins.
App manifest top-level schema (app: block)
- source: docs/app-manifest-spec.md
- type: schema
- content: Every app is a directory
apps/<id>/with amanifest.ymlcontaining a single top-levelapp:block. Apps are purely declarative — the orchestrator owns the entire lifecycle; no per-app installer code. Required fields:id(lowercase alphanumeric +-/_, must match directory name),name,version. Optional fields:description,container(ContainerConfig),dependencies(storage/app_id+version/bare string),resources(cpu_limit, memory_limit, disk_limit),security(SecurityPolicy),ports(host/container/protocol),volumes,files(GeneratedFile: path/content/overwrite; path must sit under a declared bind mount),environment(static KEY=value),health_check(type/endpoint/path/interval/timeout/retries;httpis what the monitor exercises),devices(must start with/dev/),interfaces(launch surfaces keyed by name),hooks(LifecycleHooks). Unknown keys are absorbed into anextensionsmap (serde flatten) as transitional metadata — not typed schema, not validated.
ContainerConfig schema
- source: docs/app-manifest-spec.md
- type: schema
- content: Exactly one of
imageorbuildmust be present (image XOR build). Fields:image(registry reference),image_signature(optional),pull_policy(defaultif-not-present),build({context, dockerfile default "Dockerfile", tag, build_args}),network(literal podman--networkvalue; omitted = rootless default isolated network),network_aliases(extra DNS names on the network),entrypoint,custom_args,derived_env({key, template} rendered against host facts at apply time; allowed placeholders only: {{HOST_IP}}, {{HOST_MDNS}}, {{DISK_GB}} plus dependency-resolved facts — never hard-code host specifics),secret_env({key, secret_file} read from /var/lib/archipelago/secrets/<secret_file>, injected as a podman secret so it never appears inpodman inspector unit files; secret_file must be a bare filename, no/or..),generated_secrets({name, kind} materialised by the orchestrator on first use, 0600, rootless service user, idempotent + self-healing; kind ∈ hex16|hex32|base64|bcrypt; bcrypt writes =hash and .pw=plaintext),generated_certs({crt, key, common_name?, sans?} self-signed TLS materialised before create),data_uid("UID:GID" applied to the app's bind-mounted data dir before create).
SecurityPolicy schema and validation rules
- source: docs/app-manifest-spec.md
- type: schema
- content: Security block defaults:
readonly_root: true,no_new_privileges: true,capabilities: [](cap-drop ALL, add back only listed),network_policy: isolated(isolated | bridge | host),apparmor_profile: null(optional). Validation enforced atAppManifest::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_policymust be exactly isolated/bridge/host; nocontainer:/ns:network modes; devices must be/dev/*; bind-mount sources confined to/var/lib/archipelago(reviewed exceptions: rootless podman socket and dbus);derived_envtemplates limited to the placeholder allow-list;secret_env/generated_secretsnames must be bare filenames; hook steps validated against the hook allow-list. (Note: the SPEC's documented validation list does not mention ADR-009's non-root UID, pinned-image-tag, or seccomp mandates — see INGEST-CONFLICTS.md INFO entry.)
Volumes schema
- source: docs/app-manifest-spec.md
- type: schema
- content: Volume entries:
type∈ bind | volume | tmpfs; bind entries takesource(confined to /var/lib/archipelago per validation),target,optionsfrom an allow-list (rw, ro, z, Z, shared, …); tmpfs entries taketargetandtmpfs_options(e.g. "rw,noexec,nosuid,size=256m").
Lifecycle hooks contract
- source: docs/app-manifest-spec.md
- type: api-contract
- content: Hooks are declarative, allow-listed operations that run against the app's own container — never the host (design: manifest-hooks-design.md).
post_installruns once after install with the container running; supported steps:copy_from_host(src relative to an allow-listed root — data dir / web-ui; no absolute paths, no '..') andexec(podman exec inside the container).pre_startis reserved in the schema; its executor is not yet wired.
Installation semantics (Quadlet + reconciler)
- source: docs/app-manifest-spec.md
- type: protocol
- content: The orchestrator compiles the manifest into a rootless Podman Quadlet unit under
user.slice— the container survives backend restarts and reboots. 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.
Manifest distribution channels
- source: docs/app-manifest-spec.md
- type: protocol
- content: Manifests ship two ways. (1) Signed catalog (primary):
releases/app-catalog.jsonembeds the full manifest per app with 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.ymlon 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); the marketplace uses its own flatter manifest schema, not this one. Tooling: validate withscripts/validate-app-manifest.sh, regenerate catalog withscripts/generate-app-catalog.py, drift-checked in CI byscripts/check-app-catalog-drift.py.