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,
|
/// Post-spawn hook: if a jailed agent has a matching tenant record,
|
||||||
/// call colibri-vault to provision the jail's .env before agent startup.
|
/// call colibri-vault to provision the jail's .env before agent startup.
|
||||||
pub(crate) async fn provision_tenant_env(
|
pub(crate) async fn provision_tenant_env(
|
||||||
|
|
@ -325,6 +334,18 @@ pub(crate) async fn provision_tenant_env(
|
||||||
None => return,
|
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!(
|
info!(
|
||||||
jail = jail_name,
|
jail = jail_name,
|
||||||
collection = tenant.collection_id,
|
collection = tenant.collection_id,
|
||||||
|
|
@ -462,6 +483,15 @@ async fn scheduler_tick_fn(state: &SharedState) {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn test_daemon_loop_config_defaults() {
|
fn test_daemon_loop_config_defaults() {
|
||||||
let c = DaemonLoopConfig::default();
|
let c = DaemonLoopConfig::default();
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,32 @@ use tokio::select;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use crate::spawner::{AgentSpawnConfig, JailConfig, Provider, Spawner};
|
|
||||||
use crate::daemon::provision_tenant_env;
|
use crate::daemon::provision_tenant_env;
|
||||||
|
use crate::spawner::{AgentSpawnConfig, JailConfig, Provider, Spawner};
|
||||||
use crate::{ColibriCommand, ColibriResponse, SharedState};
|
use crate::{ColibriCommand, ColibriResponse, SharedState};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Socket server
|
// 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
|
/// Prepare `socket_path` for `bind()`: remove a stale socket file, but never one
|
||||||
/// a live daemon is still serving.
|
/// a live daemon is still serving.
|
||||||
///
|
///
|
||||||
|
|
@ -444,13 +462,10 @@ async fn cmd_spawn_agent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract jail info before spawn consumes agent_config
|
// Extract jail info before spawn consumes agent_config.
|
||||||
let jail_for_provision = agent_config.jail.as_ref().and_then(|j| {
|
// Prefer the explicit jail name; only parse the Bastille root path as a
|
||||||
j.root_path.as_ref().map(|root| {
|
// fallback. The path shape is /usr/local/bastille/jails/<name>/root.
|
||||||
let name = root.split('/').nth(4).unwrap_or("unknown").to_string();
|
let jail_for_provision = agent_config.jail.as_ref().and_then(jail_provision_target);
|
||||||
(name, root.clone())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Merge extra env into agent config
|
// Merge extra env into agent config
|
||||||
let mut agent_config = agent_config;
|
let mut agent_config = agent_config;
|
||||||
|
|
@ -488,11 +503,7 @@ async fn cmd_spawn_agent(
|
||||||
let root = root.clone();
|
let root = root.clone();
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
provision_tenant_env(
|
provision_tenant_env(&state_clone, &jail_name, &root).await;
|
||||||
&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]
|
#[test]
|
||||||
fn glasspane_snapshot_command_deserializes() {
|
fn glasspane_snapshot_command_deserializes() {
|
||||||
let cmd: ColibriCommand = serde_json::from_str(r#"{"cmd":"glasspane-snapshot"}"#).unwrap();
|
let cmd: ColibriCommand = serde_json::from_str(r#"{"cmd":"glasspane-snapshot"}"#).unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue