- app-manifest-spec.md: full rewrite from the real schema in core/container/src/manifest.rs — it was 5 months stale and missing the entire modern feature set (build, network/network_aliases, derived_env, secret_env, generated_secrets/certs, data_uid, files, interfaces, hooks, extensions flatten) and documented wrong network_policy values. - app-developer-guide.md: add generated_secrets/generated_certs/ network_aliases/hooks to the field table. - APP-PACKAGING-MIGRATION-PLAN.md: phase status stamped (1-3 done, 5 mostly, 4+6 open); deleted meshtastic app removed from regression-proof lists. - registry-manifest-design.md: status design → implemented (phases 1-3), stale manifest_dir:Option line fixed. - marketplace-protocol.md: reframed proposal → as-built (marketplace.rs + RPCs + UI shipped; create-invoice noted; schema disambiguated). - manifest-hooks-design.md: phase 3 (indeedhub) done, phase 4 resolved via orchestrator+generated_secrets instead of hooks. - README/architecture: restore the NIP-07 signer-bridge claim — it is real (neode-ui/public/nostr-provider.js), the earlier audit only grepped Rust. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.4 KiB
Manifest Lifecycle Hooks — Design
Status: implemented through Phase 4 (see §6; updated 2026-07-08) — only declarative pre_start remains · originally Task #20
(indeedhub, netbird) off legacy Rust installers.
See docs/PRODUCTION-MASTER-PLAN.md, docs/APP-PACKAGING-MIGRATION-PLAN.md
("controlled hooks").
1. Problem
Some apps need a step the static manifest can't express: a post-start container
mutation. The motivating case is indeedhub's patch_indeedhub_nostr_provider():
podman exec indeedhub sed -i '/X-Frame-Options/d' /etc/nginx/conf.d/default.conf(strip the header so the app loads in our iframe)podman cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/- patch nginx conf to inject
<script src="/nostr-provider.js">and reload
A manifest files: entry writes files on the host before create; it cannot
patch a running container or copy a host file into it. Without a hook,
migrating indeedhub to the orchestrator ships a broken UI.
2. Non-goals / security posture
Per the packaging plan: NOT arbitrary host scripts. Hooks are declarative, allowlisted operations, run against the app's own (already manifest-sandboxed) container. This preserves "no arbitrary privileged execution" while giving a reviewed escape hatch.
- No host execution.
execruns inside the container (podman exec), never on the host. - No arbitrary host reads.
copy_from_host.srcis relative to an allowlist root (<data_dir>and/opt/archipelago/web-ui), resolved + canonicalised; any..escape or absolute path outside the allowlist is rejected at validate(). - Same privileges as the container.
execinherits the container's caps (already dropped persecurity:), so a hook can't exceed the app's own sandbox. - Best-effort + idempotent. Hooks must be safe to re-run (guard with
grep -q … || …). A hook failure is logged, not fatal — matching the legacy best-effort patch, so a transient hook error never bricks an install.
3. Schema (AppDefinition.hooks)
app:
id: indeedhub
hooks:
post_install: # after the container is created + running, on install
- exec: ["sed", "-i", "/X-Frame-Options/d", "/etc/nginx/conf.d/default.conf"]
- copy_from_host:
src: "web-ui/nostr-provider.js" # relative to allowlist root
dest: "/usr/share/nginx/html/nostr-provider.js"
- exec: ["sh", "-c", "grep -q nostr-provider /etc/nginx/conf.d/default.conf || sed -i 's#</head>#<script src=\"/nostr-provider.js\"></script></head>#' /etc/nginx/conf.d/default.conf"]
- exec: ["nginx", "-s", "reload"]
pre_start: [] # (future) run before each start — repair/ownership
Types (in archipelago-container):
pub enum HookStep {
Exec { exec: Vec<String> },
CopyFromHost { copy_from_host: HostCopy },
}
pub struct HostCopy { pub src: String, pub dest: String }
pub struct LifecycleHooks {
#[serde(default)] pub post_install: Vec<HookStep>,
#[serde(default)] pub pre_start: Vec<HookStep>,
}
hooks is #[serde(default)] + forward-compatible (absent = no hooks).
4. Execution
container::hooks::run_post_install(manifest, container_name, data_dir):
- Resolve container name via
compute_container_name. - For each step in order:
Exec→podman exec <container> <args…>(timeout-bounded).CopyFromHost→ canonicalisesrcagainst the allowlist roots; reject on escape;podman cp <abs-src> <container>:<dest>.
- Log each step; on error,
warn!and continue (best-effort).
Called from the orchestrator's install path after the container is up
(post-create/health), and gated so it runs on install (not every reconcile).
Validation (AppManifest::validate): every copy_from_host.src must resolve
inside an allowlist root and contain no ..; exec must be non-empty.
5. indeedhub migration (the payoff)
With hooks, indeedhub becomes fully manifest-driven: 7 member manifests
(postgres/redis/minio/relay/api/ffmpeg/frontend) + the frontend manifest carries
the post_install hook above. install_indeedhub_stack becomes orchestrator-first
(like btcpay), legacy as fallback. Same pattern unblocks netbird's setup steps.
6. Phases
- ✅ Schema + validation + unit tests —
LifecycleHooks/HookStep/HostCopyinarchipelago-container::manifest, allowlist-enforced atvalidate(). (commit4c1a4e59) - ✅ Executor + wire into orchestrator install —
container::hooks::run_post_install(exec+copy_from_host, canonicalise + symlink-escape prefix check, best-effort); called frominstall_freshafter the container is up, fresh-container-only. (commit955c54b7) - ✅ indeedhub: member manifests + frontend
post_installhooks shipped (apps/indeedhub/manifest.ymldeclares the nostr-provider copy + nginx reload;install_indeedhub_stackis orchestrator-first viainstall_stack_via_orchestrator). - ✅ netbird (resolved differently): installs via the stack orchestrator,
but its setup is handled by
generated_secrets/generated_certs+ the per-app Rustrun_pre_start_hookspath rather than manifest hooks — nohooks:block in its manifest. - ⏳
pre_starthooks (repair/ownership) — type exists; executor not yet wired. Note:prod_orchestrator.rs::run_pre_start_hooksis a hardcoded per-app Rust match today, NOT this declarative path.