use std::io::Write;
use sqlx::Database;
use crate::tenancy::error::TenancyError;
use crate::tenancy::manage::args::{next_value, reject_leading_flag};
use crate::tenancy::pools::TenantPools;
use super::users::scoped_tenant_pool;
const CREATE_HELP: &str = "create-agent <slug> <name>";
const ROTATE_HELP: &str = "rotate-agent-secret <slug> <name>";
const LIST_HELP: &str = "list-agents <slug>";
const CREATE_SKILL_HELP: &str =
"create-skill <slug> <codename> [--name <s>] [--description <s>] [--tools t1,t2] [--instructions <s>]";
const GRANT_HELP: &str = "grant-skill <slug> <agent> <skill>";
const REVOKE_HELP: &str = "revoke-skill <slug> <agent> <skill>";
const LIST_SKILLS_HELP: &str = "list-skills <slug>";
pub(super) async fn create_agent_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
reject_leading_flag(args, "create-agent", "slug", CREATE_HELP)?;
let mut iter = args.iter();
let slug = iter
.next()
.cloned()
.ok_or_else(|| TenancyError::Validation(CREATE_HELP.into()))?;
let name = iter
.next()
.cloned()
.ok_or_else(|| TenancyError::Validation(CREATE_HELP.into()))?;
if let Some(extra) = iter.next() {
return Err(TenancyError::Validation(format!(
"create-agent: unexpected argument `{extra}`"
)));
}
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
let issued = crate::tenancy::create_agent_pool(&scoped, &name)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
let id = issued.agent.id.get().copied().unwrap_or_default();
writeln!(w, "created agent `{name}` (id {id}) in tenant `{slug}`")?;
writeln!(w, " secret: {}", issued.token)?;
writeln!(w, " store this safely — it won't be shown again")?;
Ok(())
}
pub(super) async fn rotate_agent_secret_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
reject_leading_flag(args, "rotate-agent-secret", "slug", ROTATE_HELP)?;
let mut iter = args.iter();
let slug = iter
.next()
.cloned()
.ok_or_else(|| TenancyError::Validation(ROTATE_HELP.into()))?;
let name = iter
.next()
.cloned()
.ok_or_else(|| TenancyError::Validation(ROTATE_HELP.into()))?;
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
let issued = crate::tenancy::rotate_agent_secret_pool(&scoped, &name)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
writeln!(w, "rotated secret for agent `{name}` in tenant `{slug}`")?;
writeln!(w, " new secret: {}", issued.token)?;
writeln!(w, " the previous secret no longer authenticates")?;
Ok(())
}
pub(super) async fn list_agents_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
reject_leading_flag(args, "list-agents", "slug", LIST_HELP)?;
let slug = args
.first()
.cloned()
.ok_or_else(|| TenancyError::Validation(LIST_HELP.into()))?;
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
let agents = crate::tenancy::list_agents_pool(&scoped)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
if agents.is_empty() {
writeln!(w, "no agents in tenant `{slug}`")?;
return Ok(());
}
writeln!(w, "agents in tenant `{slug}`:")?;
for a in &agents {
let id = a.id.get().copied().unwrap_or_default();
let status = if a.active { "active" } else { "disabled" };
writeln!(
w,
" {id:>4} {} ({status}, prefix {})",
a.name, a.secret_prefix
)?;
}
Ok(())
}
pub(super) async fn create_skill_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
reject_leading_flag(args, "create-skill", "slug", CREATE_SKILL_HELP)?;
let mut iter = args.iter();
let slug = iter
.next()
.cloned()
.ok_or_else(|| TenancyError::Validation(CREATE_SKILL_HELP.into()))?;
let codename = iter
.next()
.cloned()
.ok_or_else(|| TenancyError::Validation(CREATE_SKILL_HELP.into()))?;
let mut name = String::new();
let mut description = String::new();
let mut instructions = String::new();
let mut tools: Vec<String> = Vec::new();
while let Some(flag) = iter.next() {
match flag.as_str() {
"--name" => name = next_value(&mut iter, "--name")?,
"--description" => description = next_value(&mut iter, "--description")?,
"--instructions" => instructions = next_value(&mut iter, "--instructions")?,
"--tools" => {
tools = next_value(&mut iter, "--tools")?
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
}
other => {
return Err(TenancyError::Validation(format!(
"create-skill: unknown argument `{other}`"
)));
}
}
}
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
let skill = crate::tenancy::create_skill_pool(
&scoped,
&codename,
&name,
&description,
&instructions,
&tools,
)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
let id = skill.id.get().copied().unwrap_or_default();
writeln!(
w,
"created skill `{codename}` (id {id}) in tenant `{slug}` with {} tool(s)",
tools.len()
)?;
Ok(())
}
pub(super) async fn grant_skill_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let (slug, agent, skill) = three_positionals(args, "grant-skill", GRANT_HELP)?;
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
crate::tenancy::grant_skill_pool(&scoped, &slug, &agent, &skill)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
writeln!(
w,
"granted skill `{skill}` to agent `{agent}` in tenant `{slug}`"
)?;
Ok(())
}
pub(super) async fn revoke_skill_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let (slug, agent, skill) = three_positionals(args, "revoke-skill", REVOKE_HELP)?;
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
crate::tenancy::revoke_skill_pool(&scoped, &slug, &agent, &skill)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
writeln!(
w,
"revoked skill `{skill}` from agent `{agent}` in tenant `{slug}`"
)?;
Ok(())
}
pub(super) async fn list_skills_cmd<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
registry_url: &str,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
reject_leading_flag(args, "list-skills", "slug", LIST_SKILLS_HELP)?;
let slug = args
.first()
.cloned()
.ok_or_else(|| TenancyError::Validation(LIST_SKILLS_HELP.into()))?;
let scoped = scoped_tenant_pool(pools, registry_url, &slug).await?;
let skills = crate::tenancy::list_skills_pool(&scoped)
.await
.map_err(|e| TenancyError::Validation(e.to_string()))?;
if skills.is_empty() {
writeln!(w, "no skills in tenant `{slug}`")?;
return Ok(());
}
writeln!(w, "skills in tenant `{slug}`:")?;
for s in &skills {
writeln!(w, " {} {}", s.codename, s.name)?;
}
Ok(())
}
fn three_positionals(
args: &[String],
verb: &str,
help: &str,
) -> Result<(String, String, String), TenancyError> {
reject_leading_flag(args, verb, "slug", help)?;
match args {
[slug, a, b] => Ok((slug.clone(), a.clone(), b.clone())),
_ => Err(TenancyError::Validation(help.into())),
}
}