From d9a404843898b1877b8ffc66f7f773f7b2b0c9ab Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 25 Aug 2026 20:06:10 +0530 Subject: [PATCH 1/3] feat(apps): add App manifest parsing crate Agentflare-Agent: claude-code Agentflare-Branch: task/528-task-1-app-manifest-parsing-crate-agentf Agentflare-Item: 528 --- Cargo.lock | 11 ++++ Cargo.toml | 2 +- crates/agentflare-apps/Cargo.toml | 17 +++++ crates/agentflare-apps/src/lib.rs | 3 + crates/agentflare-apps/src/manifest.rs | 86 ++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 crates/agentflare-apps/Cargo.toml create mode 100644 crates/agentflare-apps/src/lib.rs create mode 100644 crates/agentflare-apps/src/manifest.rs diff --git a/Cargo.lock b/Cargo.lock index 87059e41..529203e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,17 @@ dependencies = [ "toml", ] +[[package]] +name = "agentflare-apps" +version = "0.1.0" +dependencies = [ + "agentflare-gateway-registry", + "agentflare-workspace-hack", + "serde", + "tempfile", + "toml", +] + [[package]] name = "agentflare-artifacts" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 94432122..b5adc282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["agentflare-workspace-hack","crates/flare-code", "crates/agent-registry", "crates/skill-registry", "crates/gateway-registry", "crates/flare-output", "crates/agentflare-artifacts", "crates/agentflare-backend", "crates/agentflare-db-kit", "crates/flare-search-kit", "crates/agentflare-store", "crates/flare-proxy", "crates/agentflare-shim", "crates/flare-git-core", "crates/flare-process", "crates/flare-git-shim", "crates/agentflare-jobs", "crates/flare-docs", "crates/flare-vault", "crates/agentflare-resource-gate", "crates/flare-workflow", "crates/flare-sandbox"] +members = ["agentflare-workspace-hack","crates/flare-code", "crates/agent-registry", "crates/skill-registry", "crates/gateway-registry", "crates/flare-output", "crates/agentflare-artifacts", "crates/agentflare-backend", "crates/agentflare-db-kit", "crates/flare-search-kit", "crates/agentflare-store", "crates/flare-proxy", "crates/agentflare-shim", "crates/flare-git-core", "crates/flare-process", "crates/flare-git-shim", "crates/agentflare-jobs", "crates/flare-docs", "crates/flare-vault", "crates/agentflare-resource-gate", "crates/flare-workflow", "crates/flare-sandbox", "crates/agentflare-apps"] resolver = "2" [package] diff --git a/crates/agentflare-apps/Cargo.toml b/crates/agentflare-apps/Cargo.toml new file mode 100644 index 00000000..82032d03 --- /dev/null +++ b/crates/agentflare-apps/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "agentflare-apps" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +serde = { version = "1", features = ["derive"] } +toml = "0.8" +gateway-registry = { package = "agentflare-gateway-registry", path = "../gateway-registry" } +agentflare-workspace-hack = { version = "0.1", path = "../../agentflare-workspace-hack" } + +[dev-dependencies] +tempfile = "3" + +[lints.rust] +unsafe_code = "warn" diff --git a/crates/agentflare-apps/src/lib.rs b/crates/agentflare-apps/src/lib.rs new file mode 100644 index 00000000..995f09c4 --- /dev/null +++ b/crates/agentflare-apps/src/lib.rs @@ -0,0 +1,3 @@ +pub mod manifest; + +pub use manifest::{AppManifest, ToolsManifest, load_app_manifest, load_tools_manifest}; diff --git a/crates/agentflare-apps/src/manifest.rs b/crates/agentflare-apps/src/manifest.rs new file mode 100644 index 00000000..2899c338 --- /dev/null +++ b/crates/agentflare-apps/src/manifest.rs @@ -0,0 +1,86 @@ +use serde::Deserialize; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Deserialize)] +struct RawAppManifest { + name: String, + version: String, + workflow: String, + #[serde(default)] + sandbox_profile: Option, +} + +#[derive(Debug, Clone)] +pub struct AppManifest { + pub name: String, + pub version: String, + pub workflow: PathBuf, + pub sandbox_profile: Option, +} + +pub fn load_app_manifest(app_dir: &Path) -> Result { + let path = app_dir.join("app.toml"); + let text = std::fs::read_to_string(&path) + .map_err(|e| format!("could not read {}: {e}", path.display()))?; + let raw: RawAppManifest = + toml::from_str(&text).map_err(|e| format!("{}: invalid app.toml: {e}", path.display()))?; + Ok(AppManifest { + name: raw.name, + version: raw.version, + workflow: app_dir.join(raw.workflow), + sandbox_profile: raw.sandbox_profile, + }) +} + +#[derive(Debug, Deserialize)] +pub struct ToolsManifest { + #[serde(default)] + pub servers: HashMap, +} + +pub fn load_tools_manifest(app_dir: &Path) -> Result, String> { + let path = app_dir.join("tools.toml"); + if !path.is_file() { + return Ok(None); + } + let text = std::fs::read_to_string(&path) + .map_err(|e| format!("could not read {}: {e}", path.display()))?; + let parsed: ToolsManifest = + toml::from_str(&text).map_err(|e| format!("{}: invalid tools.toml: {e}", path.display()))?; + Ok(Some(parsed)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn loads_a_minimal_app_toml() { + let dir = tempfile::tempdir().unwrap(); + let mut f = std::fs::File::create(dir.path().join("app.toml")).unwrap(); + writeln!( + f, + r#" + name = "auto-company" + version = "0.1.0" + workflow = "workflow.json" + "# + ) + .unwrap(); + + let manifest = load_app_manifest(dir.path()).unwrap(); + assert_eq!(manifest.name, "auto-company"); + assert_eq!(manifest.version, "0.1.0"); + assert_eq!(manifest.workflow, dir.path().join("workflow.json")); + assert_eq!(manifest.sandbox_profile, None); + } + + #[test] + fn missing_app_toml_is_a_clear_error() { + let dir = tempfile::tempdir().unwrap(); + let err = load_app_manifest(dir.path()).unwrap_err(); + assert!(err.contains("app.toml"), "error should name the missing file: {err}"); + } +} From ef0d8369838d6d93f66da54de7405d2440888f10 Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 25 Aug 2026 20:26:31 +0530 Subject: [PATCH 2/3] feat(apps): project agent-neutral personas/skills into Claude Code layout Agentflare-Agent: claude-code Agentflare-Branch: task/531-task-3-persona-skill-projection-for-clau Agentflare-Item: 531 --- Cargo.lock | 1 + crates/agentflare-apps/Cargo.toml | 1 + crates/agentflare-apps/src/lib.rs | 1 + crates/agentflare-apps/src/project.rs | 148 ++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 crates/agentflare-apps/src/project.rs diff --git a/Cargo.lock b/Cargo.lock index 529203e0..a0ea7dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,7 @@ dependencies = [ "agentflare-gateway-registry", "agentflare-workspace-hack", "serde", + "serde_json", "tempfile", "toml", ] diff --git a/crates/agentflare-apps/Cargo.toml b/crates/agentflare-apps/Cargo.toml index 82032d03..4f8e1c7a 100644 --- a/crates/agentflare-apps/Cargo.toml +++ b/crates/agentflare-apps/Cargo.toml @@ -6,6 +6,7 @@ publish = false [dependencies] serde = { version = "1", features = ["derive"] } +serde_json = "1" toml = "0.8" gateway-registry = { package = "agentflare-gateway-registry", path = "../gateway-registry" } agentflare-workspace-hack = { version = "0.1", path = "../../agentflare-workspace-hack" } diff --git a/crates/agentflare-apps/src/lib.rs b/crates/agentflare-apps/src/lib.rs index 995f09c4..568e0ecd 100644 --- a/crates/agentflare-apps/src/lib.rs +++ b/crates/agentflare-apps/src/lib.rs @@ -1,3 +1,4 @@ pub mod manifest; +pub mod project; pub use manifest::{AppManifest, ToolsManifest, load_app_manifest, load_tools_manifest}; diff --git a/crates/agentflare-apps/src/project.rs b/crates/agentflare-apps/src/project.rs new file mode 100644 index 00000000..df124fb5 --- /dev/null +++ b/crates/agentflare-apps/src/project.rs @@ -0,0 +1,148 @@ +use crate::manifest::ToolsManifest; +use gateway_registry::ServerConfig; +use std::path::Path; + +pub fn project_for_claude_code( + app_dir: &Path, + scratch_dir: &Path, + tools: Option<&ToolsManifest>, +) -> Result<(), String> { + project_dir( + &app_dir.join("personas"), + &scratch_dir.join(".claude/agents"), + "md", + )?; + project_skills(&app_dir.join("skills"), &scratch_dir.join(".claude/skills"))?; + + let settings_dir = scratch_dir.join(".claude"); + std::fs::create_dir_all(&settings_dir).map_err(|e| e.to_string())?; + std::fs::write( + settings_dir.join("settings.json"), + serde_json::to_vec_pretty(&serde_json::json!({ "enableAllProjectMcpServers": true })) + .map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())?; + + if let Some(tools) = tools { + write_mcp_json(scratch_dir, tools)?; + } + Ok(()) +} + +fn project_dir(src: &Path, dst: &Path, ext: &str) -> Result<(), String> { + if !src.is_dir() { + return Ok(()); + } + std::fs::create_dir_all(dst).map_err(|e| e.to_string())?; + for entry in std::fs::read_dir(src).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + if entry.path().extension().is_some_and(|e| e == ext) { + let target = dst.join(entry.file_name()); + std::fs::copy(entry.path(), target).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +fn project_skills(src: &Path, dst: &Path) -> Result<(), String> { + if !src.is_dir() { + return Ok(()); + } + for entry in std::fs::read_dir(src).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.extension().is_some_and(|e| e == "md") { + let stem = path.file_stem().unwrap().to_string_lossy().into_owned(); + let skill_dir = dst.join(&stem); + std::fs::create_dir_all(&skill_dir).map_err(|e| e.to_string())?; + std::fs::copy(&path, skill_dir.join("SKILL.md")).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +fn write_mcp_json(scratch_dir: &Path, tools: &ToolsManifest) -> Result<(), String> { + let mut servers = serde_json::Map::new(); + for (name, cfg) in &tools.servers { + let entry = match cfg { + ServerConfig::McpStdio { command, args, .. } => serde_json::json!({ + "command": command, + "args": args, + }), + ServerConfig::McpHttp { url, .. } => serde_json::json!({ "url": url }), + }; + servers.insert(name.clone(), entry); + } + let doc = serde_json::json!({ "mcpServers": serde_json::Value::Object(servers) }); + std::fs::write( + scratch_dir.join(".mcp.json"), + serde_json::to_vec_pretty(&doc).map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn projects_personas_and_skills_into_claude_layout() { + let app_dir = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(app_dir.path().join("personas")).unwrap(); + std::fs::write( + app_dir.path().join("personas/ceo.md"), + "# CEO\nYou lead the company.", + ) + .unwrap(); + std::fs::create_dir_all(app_dir.path().join("skills")).unwrap(); + std::fs::write(app_dir.path().join("skills/pricing.md"), "# Pricing skill").unwrap(); + + let scratch = tempfile::tempdir().unwrap(); + project_for_claude_code(app_dir.path(), scratch.path(), None).unwrap(); + + let persona = + std::fs::read_to_string(scratch.path().join(".claude/agents/ceo.md")).unwrap(); + assert_eq!(persona, "# CEO\nYou lead the company."); + + let skill = std::fs::read_to_string(scratch.path().join(".claude/skills/pricing/SKILL.md")) + .unwrap(); + assert_eq!(skill, "# Pricing skill"); + + let settings: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(scratch.path().join(".claude/settings.json")).unwrap(), + ) + .unwrap(); + assert_eq!( + settings["enableAllProjectMcpServers"], + serde_json::json!(true) + ); + } + + #[test] + fn writes_mcp_json_when_tools_manifest_present() { + let app_dir = tempfile::tempdir().unwrap(); + let scratch = tempfile::tempdir().unwrap(); + let mut servers = std::collections::HashMap::new(); + servers.insert( + "pricing-api".to_string(), + gateway_registry::ServerConfig::McpStdio { + command: "pricing-mcp".to_string(), + args: vec![], + auth_ref: None, + auth_env: None, + }, + ); + let tools = ToolsManifest { servers }; + + project_for_claude_code(app_dir.path(), scratch.path(), Some(&tools)).unwrap(); + + let mcp_json: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(scratch.path().join(".mcp.json")).unwrap(), + ) + .unwrap(); + assert_eq!( + mcp_json["mcpServers"]["pricing-api"]["command"], + "pricing-mcp" + ); + } +} From 840d8b508ea0f8f0f607d74c95c4bb77ef4cc84d Mon Sep 17 00:00:00 2001 From: Shivakumar Date: Tue, 25 Aug 2026 22:45:08 +0530 Subject: [PATCH 3/3] chore: cargo fmt manifest.rs Agentflare-Agent: claude-code_2-1-245_agent Agentflare-Branch: task/531-task-3-persona-skill-projection-for-clau Agentflare-Item: 531 --- crates/agentflare-apps/src/manifest.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/agentflare-apps/src/manifest.rs b/crates/agentflare-apps/src/manifest.rs index 2899c338..0f64314e 100644 --- a/crates/agentflare-apps/src/manifest.rs +++ b/crates/agentflare-apps/src/manifest.rs @@ -46,8 +46,8 @@ pub fn load_tools_manifest(app_dir: &Path) -> Result, Stri } let text = std::fs::read_to_string(&path) .map_err(|e| format!("could not read {}: {e}", path.display()))?; - let parsed: ToolsManifest = - toml::from_str(&text).map_err(|e| format!("{}: invalid tools.toml: {e}", path.display()))?; + let parsed: ToolsManifest = toml::from_str(&text) + .map_err(|e| format!("{}: invalid tools.toml: {e}", path.display()))?; Ok(Some(parsed)) } @@ -81,6 +81,9 @@ mod tests { fn missing_app_toml_is_a_clear_error() { let dir = tempfile::tempdir().unwrap(); let err = load_app_manifest(dir.path()).unwrap_err(); - assert!(err.contains("app.toml"), "error should name the missing file: {err}"); + assert!( + err.contains("app.toml"), + "error should name the missing file: {err}" + ); } }