use super::helpers::*;
use crate::core::types;
use std::path::Path;
struct AgentComponent {
name: String,
component_type: String,
version: String,
machine: String,
}
pub(crate) fn cmd_agent_sbom(file: &Path, state_dir: &Path, json: bool) -> Result<(), String> {
let config = parse_and_validate(file)?;
let components = collect_agent_components(&config, state_dir);
if json {
print_agent_sbom_json(&components, &config.name);
} else {
print_agent_sbom_text(&components, &config.name);
}
Ok(())
}
fn agent_machine_label(target: &types::MachineTarget) -> String {
match target {
types::MachineTarget::Single(m) => m.clone(),
types::MachineTarget::Multiple(ms) => ms.join(","),
}
}
fn agent_component(
name: String,
component_type: &str,
version: String,
machine: String,
) -> AgentComponent {
AgentComponent {
name,
component_type: component_type.to_string(),
version,
machine,
}
}
fn model_component(id: &str, resource: &types::Resource, machine: &str) -> AgentComponent {
let version = resource
.version
.clone()
.unwrap_or_else(|| "latest".to_string());
agent_component(id.to_string(), "model", version, machine.to_string())
}
fn gpu_component(id: &str, resource: &types::Resource, machine: &str) -> AgentComponent {
let backend = resource
.gpu_backend
.clone()
.unwrap_or_else(|| "nvidia".to_string());
agent_component(id.to_string(), "gpu-runtime", backend, machine.to_string())
}
fn service_component(
id: &str,
resource: &types::Resource,
machine: &str,
) -> Option<AgentComponent> {
if !is_agent_service(id, resource) {
return None;
}
let version = resource
.version
.clone()
.unwrap_or_else(|| "unknown".to_string());
Some(agent_component(
id.to_string(),
"agent-service",
version,
machine.to_string(),
))
}
fn container_component(
id: &str,
resource: &types::Resource,
machine: &str,
) -> Option<AgentComponent> {
if !is_agent_container(id, resource) {
return None;
}
let img = resource
.image
.clone()
.unwrap_or_else(|| "unknown".to_string());
Some(agent_component(
id.to_string(),
"agent-container",
img,
machine.to_string(),
))
}
fn typed_agent_component(
id: &str,
resource: &types::Resource,
machine: &str,
) -> Option<AgentComponent> {
match resource.resource_type {
types::ResourceType::Model => Some(model_component(id, resource, machine)),
types::ResourceType::Gpu => Some(gpu_component(id, resource, machine)),
types::ResourceType::Service => service_component(id, resource, machine),
types::ResourceType::Docker => container_component(id, resource, machine),
_ => None,
}
}
fn mcp_agent_component(
id: &str,
resource: &types::Resource,
machine: String,
) -> Option<AgentComponent> {
if resource
.tags
.iter()
.any(|t| t.contains("mcp") || t.contains("pforge"))
{
Some(agent_component(
format!("{id}-mcp"),
"mcp-tool",
"registered".to_string(),
machine,
))
} else {
None
}
}
fn collect_agent_components(config: &types::ForjarConfig, state_dir: &Path) -> Vec<AgentComponent> {
let _ = state_dir;
let mut components = Vec::new();
for (id, resource) in &config.resources {
let machine = agent_machine_label(&resource.machine);
if let Some(component) = typed_agent_component(id, resource, &machine) {
components.push(component);
}
if let Some(component) = mcp_agent_component(id, resource, machine) {
components.push(component);
}
}
components.sort_by(|a, b| {
a.component_type
.cmp(&b.component_type)
.then(a.name.cmp(&b.name))
});
components
}
fn is_agent_service(id: &str, resource: &types::Resource) -> bool {
let keywords = ["mcp", "agent", "pforge", "inference", "llm"];
keywords.iter().any(|k| id.contains(k))
|| resource
.tags
.iter()
.any(|t| keywords.iter().any(|k| t.contains(k)))
}
fn is_agent_container(id: &str, resource: &types::Resource) -> bool {
let keywords = ["mcp", "agent", "inference", "llm", "model"];
keywords.iter().any(|k| id.contains(k))
|| resource
.image
.as_ref()
.is_some_and(|img| keywords.iter().any(|k| img.contains(k)))
}
fn print_agent_sbom_json(components: &[AgentComponent], name: &str) {
let items: Vec<String> = components
.iter()
.map(|c| {
format!(
r#"{{"name":"{}","type":"{}","version":"{}","machine":"{}"}}"#,
c.name, c.component_type, c.version, c.machine
)
})
.collect();
println!(
r#"{{"stack":"{}","agent_components":[{}],"total":{}}}"#,
name,
items.join(","),
components.len()
);
}
fn print_agent_sbom_text(components: &[AgentComponent], name: &str) {
println!("{}\n", bold("Agent SBOM"));
println!(" Stack: {}", bold(name));
println!(" Components: {}\n", components.len());
if components.is_empty() {
println!(" (no agent components detected)");
return;
}
let mut current_type = "";
for c in components {
if c.component_type != current_type {
current_type = &c.component_type;
println!(" {}:", bold(current_type));
}
println!(
" {} {} ({}, {})",
green("*"),
c.name,
dim(&c.version),
dim(&c.machine)
);
}
}