feat(spawner): post-spawn vault provision hook (HIVE step 3)
Some checks failed
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled

- daemon.rs: provision_tenant_env() — looks up tenant, calls colibri-vault,
  marks tenant active on success
- socket.rs: extract jail info before spawn, fire provision hook after
  agent insert (fire-and-forget via tokio::spawn)
- colibri-vault dep added to colibri-daemon Cargo.toml

After jail creation, if a tenant record matches the jail name, the hook
fetches the Vaultwarden collection and writes a 0600 .env into the jail
root before the agent starts. HIVE steps 1-3 complete.
This commit is contained in:
Hermes (debby) 2026-06-19 21:58:47 +02:00
parent 6b16281e05
commit 6cc47a55d4
4 changed files with 77 additions and 0 deletions

1
Cargo.lock generated
View file

@ -330,6 +330,7 @@ dependencies = [
"colibri-glasspane",
"colibri-runtime",
"colibri-store",
"colibri-vault",
"dashmap",
"reqwest",
"serde",

View file

@ -11,6 +11,7 @@ colibri-deepseek = { path = "../colibri-deepseek" }
colibri-glasspane = { path = "../colibri-glasspane" }
colibri-runtime = { path = "../colibri-runtime" }
colibri-store = { path = "../colibri-store" }
colibri-vault = { path = "../colibri-vault" }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["codec"] }
serde = { version = "1", features = ["derive"] }

View file

@ -298,6 +298,56 @@ async fn memory_handoff(state: &SharedState) {
);
}
/// Post-spawn hook: if a jailed agent has a matching tenant record,
/// call colibri-vault to provision the jail's .env before agent startup.
pub(crate) async fn provision_tenant_env(
state: &SharedState,
jail_name: &str,
jail_root_path: &str,
) {
// Check if this jail is a registered tenant (scope the lock)
let tenant = {
let store = state.store.lock().unwrap();
match store.get_tenant(jail_name) {
Ok(Some(t)) => Some(t),
Ok(None) => {
return; // not a tenant — nothing to provision
}
Err(e) => {
warn!(jail = jail_name, error = %e, "tenant lookup failed");
return;
}
}
};
let tenant = match tenant {
Some(t) => t,
None => return,
};
info!(
jail = jail_name,
collection = tenant.collection_id,
"provisioning tenant env from vault"
);
match colibri_vault::provision(&tenant.collection_id, jail_root_path).await {
Ok(result) => {
info!(
jail = jail_name,
items = result.items_written,
bytes = result.bytes_written,
"vault provision complete"
);
let store = state.store.lock().unwrap();
let _ = store.set_tenant_status(jail_name, "active");
}
Err(e) => {
warn!(jail = jail_name, error = %e, "vault provision failed — agent will start without .env");
}
}
}
pub async fn poll_tasks(state: &SharedState) {
debug!("task polling tick");

View file

@ -20,6 +20,7 @@ use tokio::sync::broadcast;
use tracing::{debug, error, info, trace, warn};
use crate::spawner::{AgentSpawnConfig, JailConfig, Provider, Spawner};
use crate::daemon::provision_tenant_env;
use crate::{ColibriCommand, ColibriResponse, SharedState};
// ---------------------------------------------------------------------------
@ -443,6 +444,14 @@ async fn cmd_spawn_agent(
}
}
// Extract jail info before spawn consumes agent_config
let jail_for_provision = agent_config.jail.as_ref().and_then(|j| {
j.root_path.as_ref().map(|root| {
let name = root.split('/').nth(4).unwrap_or("unknown").to_string();
(name, root.clone())
})
});
// Merge extra env into agent config
let mut agent_config = agent_config;
agent_config.env.extend(extra_env);
@ -472,6 +481,22 @@ async fn cmd_spawn_agent(
);
state.agents.insert(id.clone(), handle);
// Post-spawn hook: if agent was spawned in a jail, provision tenant env
if let Some((jail_name, root)) = &jail_for_provision {
if !jail_name.is_empty() && jail_name != "unknown" {
let jail_name = jail_name.clone();
let root = root.clone();
let state_clone = state.clone();
tokio::spawn(async move {
provision_tenant_env(
&state_clone,
&jail_name,
&root,
).await;
});
}
}
if let Some(stdout) = stdout {
let reader_state = state.clone();
let reader_agent_id = id.clone();