archy/.planning/intel/constraints.md
2026-07-29 10:45:57 -04:00

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 a manifest.yml containing a single top-level app: 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; http is what the monitor exercises), devices (must start with /dev/), interfaces (launch surfaces keyed by name), hooks (LifecycleHooks). Unknown keys are absorbed into an extensions map (serde flatten) as transitional metadata — not typed schema, not validated.

ContainerConfig schema

  • source: docs/app-manifest-spec.md
  • type: schema
  • content: Exactly one of image or build must be present (image XOR build). Fields: image (registry reference), image_signature (optional), pull_policy (default if-not-present), build ({context, dockerfile default "Dockerfile", tag, build_args}), network (literal podman --network value; 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 in podman inspect or 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 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/host; no container:/ns: network modes; devices must be /dev/*; bind-mount sources confined to /var/lib/archipelago (reviewed exceptions: rootless podman socket and dbus); derived_env templates limited to the placeholder allow-list; secret_env/generated_secrets names 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 take source (confined to /var/lib/archipelago per validation), target, options from an allow-list (rw, ro, z, Z, shared, …); tmpfs entries take target and tmpfs_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_install runs 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 '..') and exec (podman exec inside the container). pre_start is 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.json embeds 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.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); the marketplace uses its own flatter manifest schema, not this one. Tooling: validate with scripts/validate-app-manifest.sh, regenerate catalog with scripts/generate-app-catalog.py, drift-checked in CI by scripts/check-app-catalog-drift.py.