diff --git a/Cargo.lock b/Cargo.lock index 1ada89b..39f1a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,7 @@ dependencies = [ "colibri-glasspane", "colibri-runtime", "colibri-store", + "colibri-vault", "dashmap", "reqwest", "serde", diff --git a/crates/colibri-daemon/Cargo.toml b/crates/colibri-daemon/Cargo.toml index 8106fdd..61aa92c 100644 --- a/crates/colibri-daemon/Cargo.toml +++ b/crates/colibri-daemon/Cargo.toml @@ -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"] } diff --git a/crates/colibri-daemon/src/daemon.rs b/crates/colibri-daemon/src/daemon.rs index 2bc41f8..dbd4c39 100644 --- a/crates/colibri-daemon/src/daemon.rs +++ b/crates/colibri-daemon/src/daemon.rs @@ -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"); diff --git a/crates/colibri-daemon/src/socket.rs b/crates/colibri-daemon/src/socket.rs index 923de49..a5365aa 100644 --- a/crates/colibri-daemon/src/socket.rs +++ b/crates/colibri-daemon/src/socket.rs @@ -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();