fix(daemon): verify tenant provision targets (Sam & Pi) #91
2 changed files with 97 additions and 13 deletions
|
|
@ -298,6 +298,15 @@ async fn memory_handoff(state: &SharedState) {
|
|||
);
|
||||
}
|
||||
|
||||
fn trim_trailing_slash(path: &str) -> String {
|
||||
let trimmed = path.trim_end_matches('/');
|
||||
if trimmed.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
|
|
@ -325,6 +334,18 @@ pub(crate) async fn provision_tenant_env(
|
|||
None => return,
|
||||
};
|
||||
|
||||
let stored_root = trim_trailing_slash(&tenant.jail_root_path);
|
||||
let spawned_root = trim_trailing_slash(jail_root_path);
|
||||
if stored_root != spawned_root {
|
||||
warn!(
|
||||
jail = jail_name,
|
||||
tenant_root = %tenant.jail_root_path,
|
||||
spawned_root = %jail_root_path,
|
||||
"tenant root path mismatch — refusing vault provision"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
info!(
|
||||
jail = jail_name,
|
||||
collection = tenant.collection_id,
|
||||
|
|
@ -462,6 +483,15 @@ async fn scheduler_tick_fn(state: &SharedState) {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn trim_trailing_slash_handles_root_paths() {
|
||||
assert_eq!(
|
||||
trim_trailing_slash("/usr/local/bastille/jails/pi0/root/"),
|
||||
"/usr/local/bastille/jails/pi0/root"
|
||||
);
|
||||
assert_eq!(trim_trailing_slash("/"), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_daemon_loop_config_defaults() {
|
||||
let c = DaemonLoopConfig::default();
|
||||
|
|
|
|||
|
|
@ -19,14 +19,32 @@ use tokio::select;
|
|||
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::spawner::{AgentSpawnConfig, JailConfig, Provider, Spawner};
|
||||
use crate::{ColibriCommand, ColibriResponse, SharedState};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Socket server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn jail_provision_target(jail: &JailConfig) -> Option<(String, String)> {
|
||||
let root = jail.root_path.as_deref().or(jail.path.as_deref())?;
|
||||
let name = jail
|
||||
.name
|
||||
.as_deref()
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
.or_else(|| bastille_jail_name_from_root(root))?;
|
||||
Some((name.to_string(), root.to_string()))
|
||||
}
|
||||
|
||||
fn bastille_jail_name_from_root(root: &str) -> Option<&str> {
|
||||
let parts: Vec<&str> = root.split('/').filter(|part| !part.is_empty()).collect();
|
||||
parts.windows(3).find_map(|window| match window {
|
||||
["jails", name, "root"] if !name.is_empty() => Some(*name),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Prepare `socket_path` for `bind()`: remove a stale socket file, but never one
|
||||
/// a live daemon is still serving.
|
||||
///
|
||||
|
|
@ -444,13 +462,10 @@ 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())
|
||||
})
|
||||
});
|
||||
// Extract jail info before spawn consumes agent_config.
|
||||
// Prefer the explicit jail name; only parse the Bastille root path as a
|
||||
// fallback. The path shape is /usr/local/bastille/jails/<name>/root.
|
||||
let jail_for_provision = agent_config.jail.as_ref().and_then(jail_provision_target);
|
||||
|
||||
// Merge extra env into agent config
|
||||
let mut agent_config = agent_config;
|
||||
|
|
@ -488,11 +503,7 @@ async fn cmd_spawn_agent(
|
|||
let root = root.clone();
|
||||
let state_clone = state.clone();
|
||||
tokio::spawn(async move {
|
||||
provision_tenant_env(
|
||||
&state_clone,
|
||||
&jail_name,
|
||||
&root,
|
||||
).await;
|
||||
provision_tenant_env(&state_clone, &jail_name, &root).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -788,6 +799,49 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jail_provision_target_prefers_explicit_name() {
|
||||
let jail = JailConfig {
|
||||
name: Some("tenant-a".to_string()),
|
||||
root_path: Some("/usr/local/bastille/jails/tenant-a/root".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
jail_provision_target(&jail),
|
||||
Some((
|
||||
"tenant-a".to_string(),
|
||||
"/usr/local/bastille/jails/tenant-a/root".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jail_provision_target_parses_bastille_root_fallback() {
|
||||
let jail = JailConfig {
|
||||
root_path: Some("/usr/local/bastille/jails/pi0/root".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
jail_provision_target(&jail),
|
||||
Some((
|
||||
"pi0".to_string(),
|
||||
"/usr/local/bastille/jails/pi0/root".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jail_provision_target_rejects_unparseable_unnamed_root() {
|
||||
let jail = JailConfig {
|
||||
root_path: Some("/tmp/not-a-bastille-root".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(jail_provision_target(&jail), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glasspane_snapshot_command_deserializes() {
|
||||
let cmd: ColibriCommand = serde_json::from_str(r#"{"cmd":"glasspane-snapshot"}"#).unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue