feat(pine): wire the Pine 3-container stack into the orchestrator

Register "pine" as a manifest-driven stack (like netbird) across every
lifecycle site so install/stop/start/restart/uninstall/crash-recovery treat
the launcher + two Wyoming engines as one app:

- stacks.rs: pine_stack_app_ids() + install_pine_stack() (orchestrator-first,
  adopt existing, else bail — no hardcoded fallback)
- install.rs: dispatch package_id == "pine" to the stack installer
- app_ops.rs: canonical stack-member table + owning_package STACKS
- dependencies.rs: startup_order + needs_archy_net
- config.rs: all_container_names
- crash_recovery.rs: auto-start members + a StackRecoverySpec (anchor
  pine-whisper) so a genuinely-installed stack self-heals but orphan debris
  does not crash-loop
- docker_packages.rs: exclude the engines from the apps list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-21 05:14:32 -04:00
co-authored by Claude Opus 4.8
parent 1730d02003
commit 8791ef60f5
7 changed files with 81 additions and 1 deletions
@@ -496,6 +496,12 @@ pub(super) fn all_container_names(package_id: &str) -> Vec<String> {
"netbird-dashboard".into(),
"netbird-server".into(),
],
// Pine voice-assistant stack: launcher + the two Wyoming engines.
"pine" => vec![
"pine".into(),
"pine-whisper".into(),
"pine-piper".into(),
],
"nostr-vpn" => vec![
"nostr-vpn".into(),
"archy-nostr-vpn".into(),
@@ -595,6 +595,9 @@ pub(super) fn needs_archy_net(package_id: &str) -> bool {
| "nbxplorer"
| "fedimint"
| "fedimint-gateway"
| "pine"
| "pine-whisper"
| "pine-piper"
)
}
@@ -626,6 +629,7 @@ pub(super) fn startup_order(package_id: &str) -> &'static [&'static str] {
&["archy-btcpay-db", "archy-nbxplorer", "btcpay-server"]
}
"netbird" => &["netbird-server", "netbird-dashboard", "netbird"],
"pine" => &["pine-whisper", "pine-piper", "pine"],
"penpot" | "penpot-frontend" => &[
"penpot-postgres",
"penpot-valkey",
@@ -294,6 +294,9 @@ impl RpcHandler {
if package_id == "netbird" {
return self.install_netbird_stack().await;
}
if package_id == "pine" {
return self.install_pine_stack().await;
}
// Dependency checks. Prefer the scanner's cached package state so a
// congested Podman API does not turn an already-running dependency into
// a false install failure. Fall back to a bounded direct Podman probe
@@ -744,6 +744,15 @@ fn netbird_stack_app_ids() -> &'static [&'static str] {
&["netbird-server", "netbird-dashboard", "netbird"]
}
fn pine_stack_app_ids() -> &'static [&'static str] {
// Dependency/startup order: the two Wyoming engines (STT + TTS) first — they
// own their downloaded model/voice under /data and publish 10300/10200 —
// then the user-facing launcher ("pine", the nginx that serves the setup /
// status page + is the Open target). Mirrors the pine startup_order in
// dependencies.rs + the stack member table in app_ops.rs.
&["pine-whisper", "pine-piper", "pine"]
}
fn indeedhub_stack_app_ids() -> &'static [&'static str] {
// Dependency order: backends + their generated secrets first, then the api
// (owns indeedhub-jwt; reads the db/minio secrets the backends materialised),
@@ -1907,6 +1916,37 @@ impl RpcHandler {
"netbird manifests not available on this node — the signed catalog must provide apps/netbird-*/manifest.yml (legacy hardcoded installer removed in #20 ph4)"
)
}
/// Install the Pine voice-assistant stack (Whisper STT + Piper TTS + the
/// setup/status launcher). Manifest-driven only, like netbird: render the
/// 3-member stack from apps/pine-*/manifest.yml via the orchestrator
/// (archy-net + network_aliases, published Wyoming ports 10300/10200 so
/// Home Assistant reaches the engines via host.containers.internal, the
/// launcher's setup page written via `files:`). The manifests use the exact
/// live container names, so on an existing node this ADOPTS the running
/// stack rather than recreating it (downloaded models/voices preserved).
///
/// There is no in-Rust hardcoded fallback: the signed catalog always ships
/// apps/pine-*/manifest.yml. If the orchestrator doesn't know these app_ids
/// and no running stack exists to adopt, install errors rather than
/// silently diverging from the manifest contract.
pub(super) async fn install_pine_stack(&self) -> Result<serde_json::Value> {
if let Some(orchestrated) =
install_stack_via_orchestrator(self, "pine", pine_stack_app_ids()).await?
{
return Ok(orchestrated);
}
if let Some(adopted) =
adopt_stack_if_exists("pine", "pine", &["pine-whisper", "pine-piper", "pine"]).await?
{
return Ok(adopted);
}
anyhow::bail!(
"pine manifests not available on this node — the signed catalog must provide apps/pine-*/manifest.yml"
)
}
}
#[cfg(test)]