feat(hooks): manifest lifecycle-hooks schema (#20 phase 1) + fix container test literals

Add controlled post_install/pre_start hook schema to AppDefinition:
LifecycleHooks/HookStep (Exec | CopyFromHost)/HostCopy with allowlist
validation (relative src, no '..', absolute container dest, non-empty
exec). Re-exported from the crate root. Design: docs/manifest-hooks-design.md.

Also add the missing generated_secrets: vec![] field to three
pre-existing ContainerConfig test literals (the field was added to the
struct in 03a4ee1b but the container crate's own tests were never rerun,
so -p archipelago-container failed to compile). cargo test green: 53 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-21 11:07:00 -04:00
co-authored by Claude Opus 4.8
parent b0b54a96fa
commit 4c1a4e5976
4 changed files with 289 additions and 2 deletions
+47
View File
@@ -147,6 +147,53 @@ hardening; paid swarm streaming + IndeeHub source (`phase4-streaming-ecash-plan.
Meshroller Rust-native mesh AI (`meshroller-integration-design.md`); dual-ecash
phases 26 (`dual-ecash-design.md`).
## 8b. SESSION STATE + RESUME (2026-06-21, live)
**Landed + committed on main this session (newest first):**
- `f0c6b79d` immich containers named underscore (immich_server/_postgres/_redis) to
match runtime lifecycle code — fixes package.stop/start/restart. **immich fully
migrated + verified on .228** (manifest-driven stack via orchestrator).
- `b0b54a96` immich lifecycle bats suite (tests/lifecycle/bats/immich.bats).
- `d5ef4573`/`9e6c5370`/`011081d1` immich migration (rename→immich, orchestrator-first).
- `f160e0c4` podman-restart.service enabled at startup (reboot-survival).
- `0860dfac` Services-tab UI (backends→Services, parent icons, categories sub-nav, swipe).
- `220666d3`/`7bfbe8fe` registry-manifest infra phases 1+2 (consume + EMBED_MANIFESTS publish).
- `192238cb` docs consolidation 56→28 + CLAUDE.md.
- `03a4ee1b` generated-secrets system + companion/quadlet fixes.
**IN FLIGHT — hook capability (#20), phase 1 (schema):** building controlled
post-install hooks so indeedhub/netbird can migrate. Design: `docs/manifest-hooks-design.md`.
- DONE: `LifecycleHooks`/`HookStep`/`HostCopy` types + `hooks` field on AppDefinition
+ validate() + re-exports + 3 schema tests (manifest.rs).
- **BLOCKING COMPILE FIX NEEDED:** `cargo test -p archipelago-container` fails —
3 pre-existing test `ContainerConfig {…}` literals (manifest.rs ~1658/1711/1752)
are missing the `generated_secrets` field (added in 03a4ee1b but the container
crate's own tests were never run since). Add `generated_secrets: vec![],` to each.
- THEN: implement executor `core/archipelago/src/container/hooks.rs`
(run_post_install: podman exec + copy_from_host with allowlist canonicalisation),
wire into orchestrator install (post-create, install-only), tests, commit.
**NEXT (after #20):** indeedhub migration — author 7 member manifests
(postgres/redis/minio/relay/api/ffmpeg + frontend) on archy-net with container-name
hostnames; frontend carries the `post_install` hook (strip X-Frame-Options, copy
nostr-provider.js, inject script, nginx reload — see `patch_indeedhub_nostr_provider`
in install.rs:68 for exact ops); wire `install_indeedhub_stack` orchestrator-first;
generated_secrets: indeedhub-db-password/indeedhub-jwt/indeedhub-minio-password
(reuse live values); preserve hardcoded AES_MASTER_SECRET literal + minio user
"indeeadmin". Then netbird (assess its setup steps). Then single-container legacy
apps (add to `uses_orchestrator_install_flow` allowlist in install.rs + verify each).
Then the lifecycle gate (#6) — needs harness hardening (#18) + .228 bitcoin synced.
**Test/deploy facts:** .228 = archi resilience node, UI/RPC pw `password123` (https),
SSH pw `archipelago`. Lifecycle harness runs from .116: `cd tests/lifecycle &&
ARCHY_HOST=192.168.1.228 ARCHY_SCHEME=https ARCHY_PASSWORD=password123
ARCHY_ALLOW_DESTRUCTIVE=1 ./run.sh <suite>`. RPC trigger: auth.login (sets session
+ csrf cookies) → send csrf cookie value as `X-CSRF-Token` header. package.install
needs `{"id":"<app>","dockerImage":"<any-valid-image>"}` (dockerImage required even
for stacks). Rust workspace root = `core/`. Linker `undefined hidden symbol`
rebuild with `CARGO_INCREMENTAL=0`. immich on .228: app_id `immich`, containers
immich_server/immich_postgres/immich_redis, data dir owner 100998:100998.
## 9. Documentation map (what survives)
This master plan is the hub. Authoritative standalone docs (linked above), kept:
+103
View File
@@ -0,0 +1,103 @@
# Manifest Lifecycle Hooks — Design
**Status:** design (2026-06-21) · Task #20 · Prereq for migrating complex stacks
(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()`:
1. `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)
2. `podman cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/`
3. 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.** `exec` runs *inside the container* (`podman exec`), never
on the host.
- **No arbitrary host reads.** `copy_from_host.src` is **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.** `exec` inherits the container's caps
(already dropped per `security:`), 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`)
```yaml
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`):
```rust
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` → canonicalise `src` against 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
1. **Schema + executor + validation + unit tests** (this design) — `exec` +
`copy_from_host`, allowlist-enforced.
2. **Wire into orchestrator install** (post-create, install-only).
3. **indeedhub**: author member manifests + frontend `post_install` hook; wire
`install_indeedhub_stack` orchestrator-first; live-migrate + verify on .228.
4. **netbird**: assess its setup steps; migrate with hooks.
5. `pre_start` hooks (repair/ownership) if needed.