Merge feat/register-tenant-and-jail-spawn-flags — CLI register-tenant + jail spawn flags
This commit is contained in:
commit
2e6d4339eb
6 changed files with 311 additions and 21 deletions
|
|
@ -20,6 +20,7 @@ enum Command {
|
||||||
model: String,
|
model: String,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
system_prompt: Option<String>,
|
system_prompt: Option<String>,
|
||||||
|
jail: Option<colibri_client::JailConfig>,
|
||||||
},
|
},
|
||||||
KillAgent {
|
KillAgent {
|
||||||
agent_id: String,
|
agent_id: String,
|
||||||
|
|
@ -52,6 +53,12 @@ enum Command {
|
||||||
name: String,
|
name: String,
|
||||||
capabilities: Vec<String>,
|
capabilities: Vec<String>,
|
||||||
},
|
},
|
||||||
|
RegisterTenant {
|
||||||
|
tenant_id: String,
|
||||||
|
jail_root_path: String,
|
||||||
|
collection_id: String,
|
||||||
|
},
|
||||||
|
ListTenants,
|
||||||
ListAgents,
|
ListAgents,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,8 +71,8 @@ fn usage() -> &'static str {
|
||||||
colibri [--socket PATH] status
|
colibri [--socket PATH] status
|
||||||
colibri [--socket PATH] snapshot
|
colibri [--socket PATH] snapshot
|
||||||
colibri [--socket PATH] list-sessions
|
colibri [--socket PATH] list-sessions
|
||||||
colibri [--socket PATH] spawn-local EXECUTABLE [--session-id ID] [--system-prompt TEXT]
|
colibri [--socket PATH] spawn-local EXECUTABLE [--session-id ID] [--system-prompt TEXT] [--jail-name NAME [--jail-root PATH]]
|
||||||
colibri [--socket PATH] spawn-agent PROVIDER MODEL [--session-id ID] [--system-prompt TEXT]
|
colibri [--socket PATH] spawn-agent PROVIDER MODEL [--session-id ID] [--system-prompt TEXT] [--jail-name NAME [--jail-root PATH]]
|
||||||
colibri [--socket PATH] kill AGENT_ID
|
colibri [--socket PATH] kill AGENT_ID
|
||||||
colibri [--socket PATH] get-session SESSION_ID
|
colibri [--socket PATH] get-session SESSION_ID
|
||||||
colibri [--socket PATH] compact-session SESSION_ID
|
colibri [--socket PATH] compact-session SESSION_ID
|
||||||
|
|
@ -74,6 +81,8 @@ fn usage() -> &'static str {
|
||||||
colibri [--socket PATH] intake-task --title TEXT [--description TEXT] [--capability CAP]...
|
colibri [--socket PATH] intake-task --title TEXT [--description TEXT] [--capability CAP]...
|
||||||
colibri [--socket PATH] list-skills
|
colibri [--socket PATH] list-skills
|
||||||
colibri [--socket PATH] register-skill NAME [--description TEXT] [--category CAT]
|
colibri [--socket PATH] register-skill NAME [--description TEXT] [--category CAT]
|
||||||
|
colibri [--socket PATH] register-tenant TENANT_ID JAIL_ROOT_PATH COLLECTION_ID
|
||||||
|
colibri [--socket PATH] list-tenants
|
||||||
|
|
||||||
Socket defaults to COLIBRI_DAEMON_SOCKET, then the daemon's configured default.
|
Socket defaults to COLIBRI_DAEMON_SOCKET, then the daemon's configured default.
|
||||||
|
|
||||||
|
|
@ -87,7 +96,11 @@ Examples:
|
||||||
colibri register-skill freebsd-check --description "Live USB startup check" --category freebsd
|
colibri register-skill freebsd-check --description "Live USB startup check" --category freebsd
|
||||||
colibri list-skills
|
colibri list-skills
|
||||||
colibri register-agent NAME [--capability CAP]... [--capabilities CSV]
|
colibri register-agent NAME [--capability CAP]... [--capabilities CSV]
|
||||||
|
colibri register-tenant proof0 /usr/local/bastille/jails/proof0/root proof0
|
||||||
|
colibri list-tenants
|
||||||
colibri list-agents
|
colibri list-agents
|
||||||
|
# jailed spawn triggers vault provisioning into the jail root
|
||||||
|
colibri spawn-agent local /usr/local/bin/colibri-test-agent --jail-name proof0 --jail-root /usr/local/bastille/jails/proof0/root
|
||||||
"#
|
"#
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,12 +141,17 @@ where
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
Err("spawn-local requires EXECUTABLE\n\n".to_string() + usage())
|
Err("spawn-local requires EXECUTABLE\n\n".to_string() + usage())
|
||||||
} else {
|
} else {
|
||||||
let (session_id, system_prompt) = parse_spawn_options(&args[2..])?;
|
let SpawnOptions {
|
||||||
|
session_id,
|
||||||
|
system_prompt,
|
||||||
|
jail,
|
||||||
|
} = parse_spawn_options(&args[2..])?;
|
||||||
Ok(Command::SpawnAgent {
|
Ok(Command::SpawnAgent {
|
||||||
provider: "local".to_string(),
|
provider: "local".to_string(),
|
||||||
model: args[1].clone(),
|
model: args[1].clone(),
|
||||||
session_id,
|
session_id,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
|
jail,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -141,12 +159,17 @@ where
|
||||||
if args.len() < 3 {
|
if args.len() < 3 {
|
||||||
Err("spawn-agent requires PROVIDER MODEL\n\n".to_string() + usage())
|
Err("spawn-agent requires PROVIDER MODEL\n\n".to_string() + usage())
|
||||||
} else {
|
} else {
|
||||||
let (session_id, system_prompt) = parse_spawn_options(&args[3..])?;
|
let SpawnOptions {
|
||||||
|
session_id,
|
||||||
|
system_prompt,
|
||||||
|
jail,
|
||||||
|
} = parse_spawn_options(&args[3..])?;
|
||||||
Ok(Command::SpawnAgent {
|
Ok(Command::SpawnAgent {
|
||||||
provider: args[1].clone(),
|
provider: args[1].clone(),
|
||||||
model: args[2].clone(),
|
model: args[2].clone(),
|
||||||
session_id,
|
session_id,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
|
jail,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +220,22 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"register-tenant" => {
|
||||||
|
if args.len() != 4 {
|
||||||
|
Err(
|
||||||
|
"register-tenant requires TENANT_ID JAIL_ROOT_PATH COLLECTION_ID\n\n"
|
||||||
|
.to_string()
|
||||||
|
+ usage(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(Command::RegisterTenant {
|
||||||
|
tenant_id: args[1].clone(),
|
||||||
|
jail_root_path: args[2].clone(),
|
||||||
|
collection_id: args[3].clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"list-tenants" | "tenants" => expect_arity(&args, 1).map(|()| Command::ListTenants),
|
||||||
other => Err(format!("unknown command: {other}\n\n{}", usage())),
|
other => Err(format!("unknown command: {other}\n\n{}", usage())),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
|
@ -321,9 +360,11 @@ fn parse_intake_task_options(
|
||||||
Ok((title, description, capabilities))
|
Ok((title, description, capabilities))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_spawn_options(args: &[String]) -> Result<(Option<String>, Option<String>), String> {
|
fn parse_spawn_options(args: &[String]) -> Result<SpawnOptions, String> {
|
||||||
let mut session_id = None;
|
let mut session_id = None;
|
||||||
let mut system_prompt = None;
|
let mut system_prompt = None;
|
||||||
|
let mut jail_name = None;
|
||||||
|
let mut jail_root = None;
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < args.len() {
|
while i < args.len() {
|
||||||
match args[i].as_str() {
|
match args[i].as_str() {
|
||||||
|
|
@ -341,10 +382,49 @@ fn parse_spawn_options(args: &[String]) -> Result<(Option<String>, Option<String
|
||||||
system_prompt = Some(value.clone());
|
system_prompt = Some(value.clone());
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
|
// FreeBSD jail confinement. `--jail-name` enters an existing
|
||||||
|
// persistent jail (jexec); pair it with `--jail-root` so the vault
|
||||||
|
// provision hook can write `.env` into the host-visible jail root.
|
||||||
|
// This is the only flag combination that triggers vault provisioning
|
||||||
|
// after spawn (the provision hook matches on jail name + root path).
|
||||||
|
"--jail-name" => {
|
||||||
|
let Some(value) = args.get(i + 1) else {
|
||||||
|
return Err("--jail-name requires NAME\n\n".to_string() + usage());
|
||||||
|
};
|
||||||
|
jail_name = Some(value.clone());
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
"--jail-root" => {
|
||||||
|
let Some(value) = args.get(i + 1) else {
|
||||||
|
return Err("--jail-root requires PATH\n\n".to_string() + usage());
|
||||||
|
};
|
||||||
|
jail_root = Some(value.clone());
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
other => return Err(format!("unknown spawn option: {other}\n\n{}", usage())),
|
other => return Err(format!("unknown spawn option: {other}\n\n{}", usage())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((session_id, system_prompt))
|
let jail = if jail_name.is_some() || jail_root.is_some() {
|
||||||
|
Some(colibri_client::JailConfig {
|
||||||
|
name: jail_name,
|
||||||
|
root_path: jail_root,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(SpawnOptions {
|
||||||
|
session_id,
|
||||||
|
system_prompt,
|
||||||
|
jail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed `spawn-agent` / `spawn-local` options (see [`parse_spawn_options`]).
|
||||||
|
struct SpawnOptions {
|
||||||
|
session_id: Option<String>,
|
||||||
|
system_prompt: Option<String>,
|
||||||
|
jail: Option<colibri_client::JailConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_capabilities(args: &[String]) -> Result<Vec<String>, String> {
|
fn parse_capabilities(args: &[String]) -> Result<Vec<String>, String> {
|
||||||
|
|
@ -363,10 +443,21 @@ fn parse_capabilities(args: &[String]) -> Result<Vec<String>, String> {
|
||||||
let Some(value) = args.get(i + 1) else {
|
let Some(value) = args.get(i + 1) else {
|
||||||
return Err("--capabilities requires CSV\n\n".to_string() + usage());
|
return Err("--capabilities requires CSV\n\n".to_string() + usage());
|
||||||
};
|
};
|
||||||
caps.extend(value.split(',').map(str::trim).filter(|c| !c.is_empty()).map(ToString::to_string));
|
caps.extend(
|
||||||
|
value
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|c| !c.is_empty())
|
||||||
|
.map(ToString::to_string),
|
||||||
|
);
|
||||||
i += 2;
|
i += 2;
|
||||||
}
|
}
|
||||||
other => return Err(format!("unknown register-agent option: {other}\n\n{}", usage())),
|
other => {
|
||||||
|
return Err(format!(
|
||||||
|
"unknown register-agent option: {other}\n\n{}",
|
||||||
|
usage()
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(caps)
|
Ok(caps)
|
||||||
|
|
@ -414,9 +505,10 @@ async fn run(options: Options) -> Result<(), ClientError> {
|
||||||
model,
|
model,
|
||||||
session_id,
|
session_id,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
|
jail,
|
||||||
} => print_json(
|
} => print_json(
|
||||||
&client
|
&client
|
||||||
.spawn_agent(provider, model, session_id, system_prompt)
|
.spawn_agent_with(provider, model, session_id, system_prompt, jail)
|
||||||
.await?,
|
.await?,
|
||||||
),
|
),
|
||||||
Command::KillAgent { agent_id } => print_json(&client.kill_agent(agent_id).await?),
|
Command::KillAgent { agent_id } => print_json(&client.kill_agent(agent_id).await?),
|
||||||
|
|
@ -442,6 +534,16 @@ async fn run(options: Options) -> Result<(), ClientError> {
|
||||||
Command::RegisterAgent { name, capabilities } => {
|
Command::RegisterAgent { name, capabilities } => {
|
||||||
print_json(&client.register_agent(name, capabilities).await?)
|
print_json(&client.register_agent(name, capabilities).await?)
|
||||||
}
|
}
|
||||||
|
Command::RegisterTenant {
|
||||||
|
tenant_id,
|
||||||
|
jail_root_path,
|
||||||
|
collection_id,
|
||||||
|
} => print_json(
|
||||||
|
&client
|
||||||
|
.register_tenant(tenant_id, jail_root_path, collection_id)
|
||||||
|
.await?,
|
||||||
|
),
|
||||||
|
Command::ListTenants => print_json(&client.list_tenants().await?),
|
||||||
Command::ListAgents => print_json(&client.list_agents().await?),
|
Command::ListAgents => print_json(&client.list_agents().await?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -511,6 +613,7 @@ mod tests {
|
||||||
model: "/tmp/fake-agent".to_string(),
|
model: "/tmp/fake-agent".to_string(),
|
||||||
session_id: Some("s1".to_string()),
|
session_id: Some("s1".to_string()),
|
||||||
system_prompt: Some("hello".to_string()),
|
system_prompt: Some("hello".to_string()),
|
||||||
|
jail: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -579,4 +682,65 @@ mod tests {
|
||||||
let err = parse_args(["bogus"]).unwrap_err();
|
let err = parse_args(["bogus"]).unwrap_err();
|
||||||
assert!(err.contains("unknown command"));
|
assert!(err.contains("unknown command"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_spawn_local_with_jail() {
|
||||||
|
assert_eq!(
|
||||||
|
parsed(&[
|
||||||
|
"spawn-local",
|
||||||
|
"/tmp/fake-agent",
|
||||||
|
"--jail-name",
|
||||||
|
"proof0",
|
||||||
|
"--jail-root",
|
||||||
|
"/usr/local/bastille/jails/proof0/root",
|
||||||
|
]),
|
||||||
|
Options {
|
||||||
|
socket_path: default_socket_path(),
|
||||||
|
command: Command::SpawnAgent {
|
||||||
|
provider: "local".to_string(),
|
||||||
|
model: "/tmp/fake-agent".to_string(),
|
||||||
|
session_id: None,
|
||||||
|
system_prompt: None,
|
||||||
|
jail: Some(colibri_client::JailConfig {
|
||||||
|
name: Some("proof0".to_string()),
|
||||||
|
root_path: Some("/usr/local/bastille/jails/proof0/root".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_tenant_commands() {
|
||||||
|
assert_eq!(
|
||||||
|
parsed(&[
|
||||||
|
"register-tenant",
|
||||||
|
"proof0",
|
||||||
|
"/usr/local/bastille/jails/proof0/root",
|
||||||
|
"proof0",
|
||||||
|
]),
|
||||||
|
Options {
|
||||||
|
socket_path: default_socket_path(),
|
||||||
|
command: Command::RegisterTenant {
|
||||||
|
tenant_id: "proof0".to_string(),
|
||||||
|
jail_root_path: "/usr/local/bastille/jails/proof0/root".to_string(),
|
||||||
|
collection_id: "proof0".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed(&["list-tenants"]),
|
||||||
|
Options {
|
||||||
|
socket_path: default_socket_path(),
|
||||||
|
command: Command::ListTenants,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_register_tenant_wrong_arity() {
|
||||||
|
let err = parse_args(["register-tenant", "only-one"]).unwrap_err();
|
||||||
|
assert!(err.contains("register-tenant requires"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use colibri_daemon::{ColibriCommand, ColibriResponse};
|
use colibri_daemon::{ColibriCommand, ColibriResponse};
|
||||||
use colibri_glasspane::GlasspaneSnapshot;
|
use colibri_glasspane::GlasspaneSnapshot;
|
||||||
|
|
||||||
|
/// Re-export so CLI/callers can build a [`JailConfig`] for jailed spawns without
|
||||||
|
/// adding a `colibri-daemon` dependency just for the type.
|
||||||
|
pub use colibri_daemon::spawner::JailConfig;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
|
@ -102,6 +106,21 @@ impl DaemonClient {
|
||||||
model: impl Into<String>,
|
model: impl Into<String>,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
system_prompt: Option<String>,
|
system_prompt: Option<String>,
|
||||||
|
) -> Result<serde_json::Value, ClientError> {
|
||||||
|
self.spawn_agent_with(provider, model, session_id, system_prompt, None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`spawn_agent`](Self::spawn_agent) but with optional FreeBSD jail
|
||||||
|
/// confinement. Setting `jail` is the only way to trigger vault provisioning
|
||||||
|
/// (`provision_tenant_env`) after spawn — the no-jail path skips it.
|
||||||
|
pub async fn spawn_agent_with(
|
||||||
|
&self,
|
||||||
|
provider: impl Into<String>,
|
||||||
|
model: impl Into<String>,
|
||||||
|
session_id: Option<String>,
|
||||||
|
system_prompt: Option<String>,
|
||||||
|
jail: Option<JailConfig>,
|
||||||
) -> Result<serde_json::Value, ClientError> {
|
) -> Result<serde_json::Value, ClientError> {
|
||||||
self.request(&ColibriCommand::SpawnAgent {
|
self.request(&ColibriCommand::SpawnAgent {
|
||||||
provider: provider.into(),
|
provider: provider.into(),
|
||||||
|
|
@ -109,7 +128,7 @@ impl DaemonClient {
|
||||||
session_id,
|
session_id,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
local_args: None,
|
local_args: None,
|
||||||
jail: None,
|
jail,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -197,6 +216,28 @@ impl DaemonClient {
|
||||||
self.request(&ColibriCommand::ListAgents).await
|
self.request(&ColibriCommand::ListAgents).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a vault tenant — the 1:1:1 map (tenant_id = jail name =
|
||||||
|
/// Vaultwarden collection) used by `colibri-vault` to provision `.env` after
|
||||||
|
/// jail creation. Retires the raw-SQLite insert in the first-proof runbook.
|
||||||
|
pub async fn register_tenant(
|
||||||
|
&self,
|
||||||
|
tenant_id: impl Into<String>,
|
||||||
|
jail_root_path: impl Into<String>,
|
||||||
|
collection_id: impl Into<String>,
|
||||||
|
) -> Result<serde_json::Value, ClientError> {
|
||||||
|
self.request(&ColibriCommand::RegisterTenant {
|
||||||
|
tenant_id: tenant_id.into(),
|
||||||
|
jail_root_path: jail_root_path.into(),
|
||||||
|
collection_id: collection_id.into(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all registered tenants (status verification without raw SQLite).
|
||||||
|
pub async fn list_tenants(&self) -> Result<serde_json::Value, ClientError> {
|
||||||
|
self.request(&ColibriCommand::ListTenants).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn register_skill(
|
pub async fn register_skill(
|
||||||
&self,
|
&self,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
|
|
|
||||||
|
|
@ -380,3 +380,45 @@ async fn harness_double_spawn_session_isolation() {
|
||||||
server.await.unwrap();
|
server.await.unwrap();
|
||||||
let _ = tokio::fs::remove_dir_all(config.data_dir).await;
|
let _ = tokio::fs::remove_dir_all(config.data_dir).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn register_tenant_and_list_over_socket() {
|
||||||
|
// Retires the raw-SQLite insert in the first-proof runbook: register-tenant
|
||||||
|
// + list-tenants now flow through the socket, returning the tenant record.
|
||||||
|
let config = check_config();
|
||||||
|
tokio::fs::create_dir_all(&config.data_dir).await.unwrap();
|
||||||
|
|
||||||
|
let state: SharedState = Arc::new(DaemonState::new(config.clone()));
|
||||||
|
let shutdown = state.shutdown_rx.resubscribe();
|
||||||
|
let server_state = state.clone();
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let _ = socket::serve(server_state, shutdown).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let client = DaemonClient::new(config.socket_path.clone());
|
||||||
|
wait_for_socket(&client).await;
|
||||||
|
|
||||||
|
let registered = client
|
||||||
|
.register_tenant("proof0", "/usr/local/bastille/jails/proof0/root", "proof0")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(registered["tenant_id"], "proof0");
|
||||||
|
assert_eq!(registered["status"], "provisioned");
|
||||||
|
assert_eq!(
|
||||||
|
registered["jail_root_path"],
|
||||||
|
"/usr/local/bastille/jails/proof0/root"
|
||||||
|
);
|
||||||
|
|
||||||
|
let listed = client.list_tenants().await.unwrap();
|
||||||
|
let found = listed
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|t| t["tenant_id"] == "proof0")
|
||||||
|
.expect("registered tenant appears in list-tenants");
|
||||||
|
assert_eq!(found["collection_id"], "proof0");
|
||||||
|
|
||||||
|
let _ = state.shutdown_tx.send(());
|
||||||
|
server.await.unwrap();
|
||||||
|
let _ = tokio::fs::remove_dir_all(config.data_dir).await;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,19 @@ pub enum ColibriCommand {
|
||||||
},
|
},
|
||||||
#[serde(rename = "set-cost-mode")]
|
#[serde(rename = "set-cost-mode")]
|
||||||
SetCostMode { mode: String },
|
SetCostMode { mode: String },
|
||||||
|
// ── Vault tenants ─────────────────────────────────────────
|
||||||
|
/// Register a tenant: the 1:1:1 map (tenant_id = jail name = Vaultwarden
|
||||||
|
/// collection) that colibri-vault uses to know where to write `.env` after
|
||||||
|
/// jail creation. Wires the existing `store::register_tenant` to the socket
|
||||||
|
/// + CLI so the first-proof runbook no longer needs raw SQLite.
|
||||||
|
#[serde(rename = "register-tenant")]
|
||||||
|
RegisterTenant {
|
||||||
|
tenant_id: String,
|
||||||
|
jail_root_path: String,
|
||||||
|
collection_id: String,
|
||||||
|
},
|
||||||
|
#[serde(rename = "list-tenants")]
|
||||||
|
ListTenants,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outbound control-plane response.
|
/// Outbound control-plane response.
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,12 @@ async fn dispatch(cmd: ColibriCommand, state: &SharedState) -> ColibriResponse {
|
||||||
capabilities,
|
capabilities,
|
||||||
} => cmd_intake_task(state, title, description, capabilities).await,
|
} => cmd_intake_task(state, title, description, capabilities).await,
|
||||||
ColibriCommand::SetCostMode { mode } => cmd_set_cost_mode(state, mode).await,
|
ColibriCommand::SetCostMode { mode } => cmd_set_cost_mode(state, mode).await,
|
||||||
|
ColibriCommand::RegisterTenant {
|
||||||
|
tenant_id,
|
||||||
|
jail_root_path,
|
||||||
|
collection_id,
|
||||||
|
} => cmd_register_tenant(state, tenant_id, jail_root_path, collection_id).await,
|
||||||
|
ColibriCommand::ListTenants => cmd_list_tenants(state).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -701,6 +707,30 @@ async fn cmd_register_agent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn cmd_register_tenant(
|
||||||
|
state: &SharedState,
|
||||||
|
tenant_id: String,
|
||||||
|
jail_root_path: String,
|
||||||
|
collection_id: String,
|
||||||
|
) -> ColibriResponse {
|
||||||
|
match state
|
||||||
|
.store
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.register_tenant(&tenant_id, &jail_root_path, &collection_id)
|
||||||
|
{
|
||||||
|
Ok(tenant) => ColibriResponse::ok(serde_json::to_value(tenant).unwrap_or_default()),
|
||||||
|
Err(e) => ColibriResponse::err(format!("register tenant failed: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cmd_list_tenants(state: &SharedState) -> ColibriResponse {
|
||||||
|
match state.store.lock().unwrap().list_tenants() {
|
||||||
|
Ok(tenants) => ColibriResponse::ok(serde_json::to_value(tenants).unwrap_or_default()),
|
||||||
|
Err(e) => ColibriResponse::err(format!("list tenants failed: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn cmd_list_skills(state: &SharedState) -> ColibriResponse {
|
async fn cmd_list_skills(state: &SharedState) -> ColibriResponse {
|
||||||
match state.store.lock().unwrap().list_skills() {
|
match state.store.lock().unwrap().list_skills() {
|
||||||
Ok(skills) => ColibriResponse::ok(serde_json::to_value(skills).unwrap_or_default()),
|
Ok(skills) => ColibriResponse::ok(serde_json::to_value(skills).unwrap_or_default()),
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,17 @@ struct SerdeLogin {
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate and normalize a key for .env output.
|
||||||
|
/// Reject keys with non-[A-Z0-9_] characters (matching the clawdie-vault-fetch helper).
|
||||||
|
fn validate_key(raw: &str) -> String {
|
||||||
|
let key = raw.to_uppercase().replace([' ', '-', '.'], "_");
|
||||||
|
if key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !key.is_empty() {
|
||||||
|
key
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -466,14 +477,3 @@ mod tests {
|
||||||
assert!(validate_key("").is_empty());
|
assert!(validate_key("").is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validate and normalize a key for .env output.
|
|
||||||
/// Reject keys with non-[A-Z0-9_] characters (matching the clawdie-vault-fetch helper).
|
|
||||||
fn validate_key(raw: &str) -> String {
|
|
||||||
let key = raw.to_uppercase().replace([' ', '-', '.'], "_");
|
|
||||||
if key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !key.is_empty() {
|
|
||||||
key
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue