From 8ec7e5a559e36db75365c153a4c9866d2d541518 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:09:48 +0400 Subject: [PATCH 01/12] phase 7: add template metadata model --- backend/controlplane/src/lib.rs | 1 + backend/controlplane/src/marketplace/mod.rs | 5 ++ backend/controlplane/src/marketplace/model.rs | 60 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 backend/controlplane/src/marketplace/mod.rs create mode 100644 backend/controlplane/src/marketplace/model.rs diff --git a/backend/controlplane/src/lib.rs b/backend/controlplane/src/lib.rs index dda7495..40b974a 100644 --- a/backend/controlplane/src/lib.rs +++ b/backend/controlplane/src/lib.rs @@ -19,6 +19,7 @@ pub mod constants; pub mod error; pub mod gateway; pub mod governance; +pub mod marketplace; pub mod mcp; pub mod observability; pub mod registry; diff --git a/backend/controlplane/src/marketplace/mod.rs b/backend/controlplane/src/marketplace/mod.rs new file mode 100644 index 0000000..52826dc --- /dev/null +++ b/backend/controlplane/src/marketplace/mod.rs @@ -0,0 +1,5 @@ +//! Agent Marketplace — a verified internal catalogue of reusable agent templates. + +pub mod model; + +pub use model::AgentTemplate; diff --git a/backend/controlplane/src/marketplace/model.rs b/backend/controlplane/src/marketplace/model.rs new file mode 100644 index 0000000..1b09b91 --- /dev/null +++ b/backend/controlplane/src/marketplace/model.rs @@ -0,0 +1,60 @@ +//! Agent marketplace domain model. +//! +//! The marketplace is a verified, internal catalogue of reusable agent +//! templates. Publishing puts an agent blueprint on the shelf; installing +//! stamps out a concrete agent into the [`registry`](crate::registry). + +use serde::{Deserialize, Serialize}; + +use crate::constants::{DataAccessLevel, RiskLevel}; +use crate::registry::NewAgent; + +/// The reusable blueprint behind a marketplace listing: everything needed to +/// instantiate a concrete agent. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentTemplate { + /// Framework the instantiated agent runs on. + pub framework: String, + /// Default model provider. + pub model_provider: String, + /// Default model name. + pub model_name: String, + /// Tools the template requires. + #[serde(default)] + pub required_tools: Vec, + /// MCP servers the template requires. + #[serde(default)] + pub required_mcp_servers: Vec, + /// Model providers the template is approved against. + #[serde(default)] + pub required_model_providers: Vec, + /// Data sensitivity the instantiated agent will access. + pub data_access_level: DataAccessLevel, + /// Risk level of the instantiated agent. + pub risk_level: RiskLevel, +} + +impl AgentTemplate { + /// Produce a [`NewAgent`] from this template for the given owner/department. + pub fn to_new_agent( + &self, + name: impl Into, + description: impl Into, + owner: impl Into, + department: impl Into, + ) -> NewAgent { + NewAgent { + name: name.into(), + description: description.into(), + owner: owner.into(), + department: department.into(), + framework: self.framework.clone(), + model_provider: self.model_provider.clone(), + model_name: self.model_name.clone(), + tools_allowed: self.required_tools.clone(), + mcp_servers_allowed: self.required_mcp_servers.clone(), + data_access_level: self.data_access_level, + risk_level: self.risk_level, + } + } +} From 212896032629472729b0af328dc9b759ed49f792 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:10:13 +0400 Subject: [PATCH 02/12] phase 7: add marketplace agent model --- backend/controlplane/src/marketplace/mod.rs | 2 +- backend/controlplane/src/marketplace/model.rs | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/backend/controlplane/src/marketplace/mod.rs b/backend/controlplane/src/marketplace/mod.rs index 52826dc..873df8a 100644 --- a/backend/controlplane/src/marketplace/mod.rs +++ b/backend/controlplane/src/marketplace/mod.rs @@ -2,4 +2,4 @@ pub mod model; -pub use model::AgentTemplate; +pub use model::{AgentTemplate, ComplianceBadge, MarketplaceAgent, NewListing, VerificationBadge}; diff --git a/backend/controlplane/src/marketplace/model.rs b/backend/controlplane/src/marketplace/model.rs index 1b09b91..070dd29 100644 --- a/backend/controlplane/src/marketplace/model.rs +++ b/backend/controlplane/src/marketplace/model.rs @@ -4,11 +4,98 @@ //! templates. Publishing puts an agent blueprint on the shelf; installing //! stamps out a concrete agent into the [`registry`](crate::registry). +use chrono::Utc; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::constants::{DataAccessLevel, RiskLevel}; use crate::registry::NewAgent; +/// Verification badge — has the listing been vetted by the platform team? +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum VerificationBadge { + /// Not yet reviewed. + Unverified, + /// Reviewed and verified by the platform team. + Verified, +} + +/// Compliance badge — where the listing sits in the compliance review process. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ComplianceBadge { + /// Compliance review pending. + Pending, + /// Passed compliance review. + Compliant, + /// Formally certified for regulated use. + Certified, +} + +/// A published marketplace listing wrapping a reusable [`AgentTemplate`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketplaceAgent { + /// Stable UUID. + pub id: String, + pub name: String, + pub description: String, + /// Functional category (e.g. `licensing`, `it-ops`, `customer-service`). + pub category: String, + /// Owning department. + pub department: String, + /// Average user rating (0.0–5.0). + pub rating: f64, + /// Number of times the listing has been installed. + pub install_count: u64, + /// Risk badge of the template. + pub risk_level: RiskLevel, + /// Verification status. + pub verification: VerificationBadge, + /// Compliance status. + pub compliance: ComplianceBadge, + /// The reusable blueprint. + pub template: AgentTemplate, + pub published_at: i64, +} + +/// Input used to publish a new listing. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewListing { + pub name: String, + pub description: String, + pub category: String, + pub department: String, + pub template: AgentTemplate, +} + +impl MarketplaceAgent { + /// Materialise a new listing: unverified, pending compliance, zero installs. + pub fn from_new(input: NewListing) -> Self { + MarketplaceAgent { + id: Uuid::new_v4().to_string(), + name: input.name, + description: input.description, + category: input.category, + department: input.department, + rating: 0.0, + install_count: 0, + risk_level: input.template.risk_level, + verification: VerificationBadge::Unverified, + compliance: ComplianceBadge::Pending, + template: input.template, + published_at: Utc::now().timestamp(), + } + } + + /// Whether this listing is safe to surface as a trusted, install-ready + /// option: verified and at least compliance-reviewed. + pub fn is_trusted(&self) -> bool { + self.verification == VerificationBadge::Verified + && matches!(self.compliance, ComplianceBadge::Compliant | ComplianceBadge::Certified) + } +} + /// The reusable blueprint behind a marketplace listing: everything needed to /// instantiate a concrete agent. #[derive(Debug, Clone, Serialize, Deserialize)] From dec02aaa53e1e87dbb382ac35b958dea1284f02b Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:10:52 +0400 Subject: [PATCH 03/12] phase 7: add publish agent to marketplace --- backend/controlplane/src/marketplace/mod.rs | 2 + backend/controlplane/src/marketplace/store.rs | 162 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 backend/controlplane/src/marketplace/store.rs diff --git a/backend/controlplane/src/marketplace/mod.rs b/backend/controlplane/src/marketplace/mod.rs index 873df8a..327397b 100644 --- a/backend/controlplane/src/marketplace/mod.rs +++ b/backend/controlplane/src/marketplace/mod.rs @@ -1,5 +1,7 @@ //! Agent Marketplace — a verified internal catalogue of reusable agent templates. pub mod model; +pub mod store; pub use model::{AgentTemplate, ComplianceBadge, MarketplaceAgent, NewListing, VerificationBadge}; +pub use store::Marketplace; diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs new file mode 100644 index 0000000..3e63f5c --- /dev/null +++ b/backend/controlplane/src/marketplace/store.rs @@ -0,0 +1,162 @@ +//! SQLite-backed marketplace store. +//! +//! Holds published listings. Installing a listing is delegated to the caller's +//! [`AgentRegistry`](crate::registry::AgentRegistry) so the marketplace stays +//! decoupled from agent persistence. + +use std::sync::Mutex; + +use rusqlite::{params, Connection}; + +use crate::error::{ControlPlaneError, Result}; + +use super::model::{MarketplaceAgent, NewListing}; + +/// Store of published marketplace listings. +pub struct Marketplace { + pub(crate) conn: Mutex, +} + +const SCHEMA: &str = " + CREATE TABLE IF NOT EXISTS marketplace_listings ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT NOT NULL, + category TEXT NOT NULL, + department TEXT NOT NULL, + rating REAL NOT NULL, + install_count INTEGER NOT NULL, + risk_level TEXT NOT NULL, + verification TEXT NOT NULL, + compliance TEXT NOT NULL, + template TEXT NOT NULL, + published_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_mkt_category ON marketplace_listings(category); +"; + +const COLUMNS: &str = "id, name, description, category, department, rating, install_count, \ + risk_level, verification, compliance, template, published_at"; + +fn row_to_listing(row: &rusqlite::Row) -> rusqlite::Result { + Ok(MarketplaceAgent { + id: row.get(0)?, + name: row.get(1)?, + description: row.get(2)?, + category: row.get(3)?, + department: row.get(4)?, + rating: row.get(5)?, + install_count: row.get::<_, i64>(6)? as u64, + risk_level: de(&row.get::<_, String>(7)?, 7)?, + verification: de(&row.get::<_, String>(8)?, 8)?, + compliance: de(&row.get::<_, String>(9)?, 9)?, + template: de(&row.get::<_, String>(10)?, 10)?, + published_at: row.get(11)?, + }) +} + +fn de(s: &str, col: usize) -> rusqlite::Result { + serde_json::from_str(s) + .map_err(|e| rusqlite::Error::FromSqlConversionFailure(col, rusqlite::types::Type::Text, Box::new(e))) +} + +impl Marketplace { + /// Open (creating if needed) a marketplace backed by a file. + pub fn open(path: &str) -> Result { + let conn = Connection::open(path)?; + conn.execute_batch(&format!("PRAGMA journal_mode=WAL;{SCHEMA}"))?; + Ok(Self { conn: Mutex::new(conn) }) + } + + /// Open an ephemeral in-memory marketplace (used by tests). + pub fn in_memory() -> Result { + let conn = Connection::open_in_memory()?; + conn.execute_batch(SCHEMA)?; + Ok(Self { conn: Mutex::new(conn) }) + } + + /// Publish a new listing to the marketplace. + pub fn publish(&self, input: NewListing) -> Result { + if input.name.trim().is_empty() { + return Err(ControlPlaneError::validation("listing name must not be empty")); + } + let listing = MarketplaceAgent::from_new(input); + self.upsert(&listing)?; + cp_info!("marketplace.publish", listing_id = %listing.id, name = %listing.name); + Ok(listing) + } + + /// Fetch a listing by id. + pub fn get(&self, id: &str) -> Result { + let conn = self.conn.lock().expect("marketplace mutex poisoned"); + conn.query_row( + &format!("SELECT {COLUMNS} FROM marketplace_listings WHERE id = ?1"), + params![id], + row_to_listing, + ) + .map_err(|e| match e { + rusqlite::Error::QueryReturnedNoRows => ControlPlaneError::not_found("listing", id), + other => other.into(), + }) + } + + /// Persist a listing (insert or replace). Internal helper. + pub(crate) fn upsert(&self, l: &MarketplaceAgent) -> Result<()> { + let conn = self.conn.lock().expect("marketplace mutex poisoned"); + conn.execute( + "INSERT OR REPLACE INTO marketplace_listings ( + id, name, description, category, department, rating, install_count, + risk_level, verification, compliance, template, published_at + ) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)", + params![ + l.id, + l.name, + l.description, + l.category, + l.department, + l.rating, + l.install_count as i64, + serde_json::to_string(&l.risk_level)?, + serde_json::to_string(&l.verification)?, + serde_json::to_string(&l.compliance)?, + serde_json::to_string(&l.template)?, + l.published_at, + ], + )?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::constants::{DataAccessLevel, RiskLevel}; + use crate::marketplace::model::AgentTemplate; + + pub(super) fn listing() -> NewListing { + NewListing { + name: "Permit Intake".into(), + description: "Triages permit applications".into(), + category: "licensing".into(), + department: "Licensing".into(), + template: AgentTemplate { + framework: "openclaw".into(), + model_provider: "anthropic".into(), + model_name: "claude-opus-4-8".into(), + required_tools: vec!["search".into()], + required_mcp_servers: vec!["records-mcp".into()], + required_model_providers: vec!["anthropic".into()], + data_access_level: DataAccessLevel::Internal, + risk_level: RiskLevel::Medium, + }, + } + } + + #[test] + fn publish_then_get() { + let mkt = Marketplace::in_memory().unwrap(); + let l = mkt.publish(listing()).unwrap(); + assert_eq!(mkt.get(&l.id).unwrap().name, "Permit Intake"); + assert!(!l.is_trusted()); // unverified + pending on publish + } +} From 78aecc7c4076a21637bb2bbd58ecf2664ffc568c Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:11:05 +0400 Subject: [PATCH 04/12] phase 7: add list marketplace agents --- backend/controlplane/src/marketplace/store.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index 3e63f5c..0a5ee23 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -86,6 +86,23 @@ impl Marketplace { Ok(listing) } + /// List all listings, most-installed first. + pub fn list(&self) -> Result> { + self.query(&format!("SELECT {COLUMNS} FROM marketplace_listings ORDER BY install_count DESC"), []) + } + + /// Run a SELECT returning listings (internal helper). + fn query(&self, sql: &str, params: P) -> Result> { + let conn = self.conn.lock().expect("marketplace mutex poisoned"); + let mut stmt = conn.prepare(sql)?; + let rows = stmt.query_map(params, row_to_listing)?; + let mut out = Vec::new(); + for r in rows { + out.push(r?); + } + Ok(out) + } + /// Fetch a listing by id. pub fn get(&self, id: &str) -> Result { let conn = self.conn.lock().expect("marketplace mutex poisoned"); From b25a630df16165c57e5c8e2acad4443a748d1711 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:11:15 +0400 Subject: [PATCH 05/12] phase 7: add filter by category --- backend/controlplane/src/marketplace/store.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index 0a5ee23..ff2ae19 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -91,6 +91,14 @@ impl Marketplace { self.query(&format!("SELECT {COLUMNS} FROM marketplace_listings ORDER BY install_count DESC"), []) } + /// List listings in a given category. + pub fn list_by_category(&self, category: &str) -> Result> { + self.query( + &format!("SELECT {COLUMNS} FROM marketplace_listings WHERE category = ?1 ORDER BY install_count DESC"), + params![category], + ) + } + /// Run a SELECT returning listings (internal helper). fn query(&self, sql: &str, params: P) -> Result> { let conn = self.conn.lock().expect("marketplace mutex poisoned"); From 4ab7525b0ebf9a69024b5e9d32e43b29ee16a7ac Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:11:36 +0400 Subject: [PATCH 06/12] phase 7: add filter by risk level --- backend/controlplane/src/marketplace/store.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index ff2ae19..a6eadc7 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -8,6 +8,7 @@ use std::sync::Mutex; use rusqlite::{params, Connection}; +use crate::constants::RiskLevel; use crate::error::{ControlPlaneError, Result}; use super::model::{MarketplaceAgent, NewListing}; @@ -99,6 +100,14 @@ impl Marketplace { ) } + /// List listings at a given risk level. + pub fn list_by_risk(&self, risk: RiskLevel) -> Result> { + self.query( + &format!("SELECT {COLUMNS} FROM marketplace_listings WHERE risk_level = ?1 ORDER BY install_count DESC"), + params![serde_json::to_string(&risk)?], + ) + } + /// Run a SELECT returning listings (internal helper). fn query(&self, sql: &str, params: P) -> Result> { let conn = self.conn.lock().expect("marketplace mutex poisoned"); @@ -155,7 +164,7 @@ impl Marketplace { #[cfg(test)] mod tests { use super::*; - use crate::constants::{DataAccessLevel, RiskLevel}; + use crate::constants::DataAccessLevel; use crate::marketplace::model::AgentTemplate; pub(super) fn listing() -> NewListing { From 7d840bc0e92aee155e54b46f9080fc78950edbf9 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:11:56 +0400 Subject: [PATCH 07/12] phase 7: add install agent from template --- backend/controlplane/src/marketplace/store.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index a6eadc7..26f9177 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -10,6 +10,7 @@ use rusqlite::{params, Connection}; use crate::constants::RiskLevel; use crate::error::{ControlPlaneError, Result}; +use crate::registry::{AgentRecord, AgentRegistry}; use super::model::{MarketplaceAgent, NewListing}; @@ -120,6 +121,27 @@ impl Marketplace { Ok(out) } + /// Install a listing into the given agent registry, returning the new + /// agent record and incrementing the listing's install count. + pub fn install( + &self, + listing_id: &str, + registry: &AgentRegistry, + name: &str, + owner: &str, + department: &str, + ) -> Result { + let mut listing = self.get(listing_id)?; + let new_agent = listing + .template + .to_new_agent(name, listing.description.clone(), owner, department); + let agent = registry.create(new_agent)?; + listing.install_count += 1; + self.upsert(&listing)?; + cp_info!("marketplace.install", listing_id = %listing_id, agent_id = %agent.id); + Ok(agent) + } + /// Fetch a listing by id. pub fn get(&self, id: &str) -> Result { let conn = self.conn.lock().expect("marketplace mutex poisoned"); From a9bdb90887e1f2958401715b16e04a52a62e6a5b Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:12:17 +0400 Subject: [PATCH 08/12] phase 7: add verification badge --- backend/controlplane/src/marketplace/store.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index 26f9177..86e9b79 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -12,7 +12,7 @@ use crate::constants::RiskLevel; use crate::error::{ControlPlaneError, Result}; use crate::registry::{AgentRecord, AgentRegistry}; -use super::model::{MarketplaceAgent, NewListing}; +use super::model::{MarketplaceAgent, NewListing, VerificationBadge}; /// Store of published marketplace listings. pub struct Marketplace { @@ -142,6 +142,15 @@ impl Marketplace { Ok(agent) } + /// Set the verification badge on a listing (platform-team action). + pub fn set_verification(&self, listing_id: &str, badge: VerificationBadge) -> Result { + let mut listing = self.get(listing_id)?; + listing.verification = badge; + self.upsert(&listing)?; + cp_info!("marketplace.verify", listing_id = %listing_id, badge = ?badge); + Ok(listing) + } + /// Fetch a listing by id. pub fn get(&self, id: &str) -> Result { let conn = self.conn.lock().expect("marketplace mutex poisoned"); From b3d3f551a705330c8766efb571bc49b19f748fd9 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:12:30 +0400 Subject: [PATCH 09/12] phase 7: add compliance badge --- backend/controlplane/src/marketplace/store.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index 86e9b79..56dcdea 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -12,7 +12,7 @@ use crate::constants::RiskLevel; use crate::error::{ControlPlaneError, Result}; use crate::registry::{AgentRecord, AgentRegistry}; -use super::model::{MarketplaceAgent, NewListing, VerificationBadge}; +use super::model::{ComplianceBadge, MarketplaceAgent, NewListing, VerificationBadge}; /// Store of published marketplace listings. pub struct Marketplace { @@ -151,6 +151,15 @@ impl Marketplace { Ok(listing) } + /// Set the compliance badge on a listing (compliance-team action). + pub fn set_compliance(&self, listing_id: &str, badge: ComplianceBadge) -> Result { + let mut listing = self.get(listing_id)?; + listing.compliance = badge; + self.upsert(&listing)?; + cp_info!("marketplace.compliance", listing_id = %listing_id, badge = ?badge); + Ok(listing) + } + /// Fetch a listing by id. pub fn get(&self, id: &str) -> Result { let conn = self.conn.lock().expect("marketplace mutex poisoned"); From 658ac591db8d7899257b59e6a40058e140d9ad29 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:12:55 +0400 Subject: [PATCH 10/12] phase 7: add marketplace seed data --- backend/controlplane/src/marketplace/mod.rs | 2 + backend/controlplane/src/marketplace/seed.rs | 71 ++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 backend/controlplane/src/marketplace/seed.rs diff --git a/backend/controlplane/src/marketplace/mod.rs b/backend/controlplane/src/marketplace/mod.rs index 327397b..2c875cb 100644 --- a/backend/controlplane/src/marketplace/mod.rs +++ b/backend/controlplane/src/marketplace/mod.rs @@ -1,7 +1,9 @@ //! Agent Marketplace — a verified internal catalogue of reusable agent templates. pub mod model; +pub mod seed; pub mod store; pub use model::{AgentTemplate, ComplianceBadge, MarketplaceAgent, NewListing, VerificationBadge}; +pub use seed::{sample_listings, seed}; pub use store::Marketplace; diff --git a/backend/controlplane/src/marketplace/seed.rs b/backend/controlplane/src/marketplace/seed.rs new file mode 100644 index 0000000..0096ba0 --- /dev/null +++ b/backend/controlplane/src/marketplace/seed.rs @@ -0,0 +1,71 @@ +//! Sample marketplace listings for demos and local development. + +use crate::constants::{DataAccessLevel, RiskLevel}; +use crate::error::Result; + +use super::model::{AgentTemplate, ComplianceBadge, MarketplaceAgent, NewListing, VerificationBadge}; +use super::store::Marketplace; + +/// The built-in catalogue of example listings. +pub fn sample_listings() -> Vec { + vec![ + NewListing { + name: "Permit Intake Assistant".into(), + description: "Triages building-permit applications and routes them.".into(), + category: "licensing".into(), + department: "Licensing".into(), + template: AgentTemplate { + framework: "openclaw".into(), + model_provider: "anthropic".into(), + model_name: "claude-opus-4-8".into(), + required_tools: vec!["search".into(), "document.read".into()], + required_mcp_servers: vec!["records-mcp".into()], + required_model_providers: vec!["anthropic".into()], + data_access_level: DataAccessLevel::Internal, + risk_level: RiskLevel::Medium, + }, + }, + NewListing { + name: "Service Desk Responder".into(), + description: "Answers common citizen service-desk questions.".into(), + category: "customer-service".into(), + department: "Customer Happiness".into(), + template: AgentTemplate { + framework: "openclaw".into(), + model_provider: "anthropic".into(), + model_name: "claude-sonnet-4-6".into(), + required_tools: vec!["search".into()], + required_mcp_servers: vec![], + required_model_providers: vec!["anthropic".into()], + data_access_level: DataAccessLevel::Public, + risk_level: RiskLevel::Low, + }, + }, + ] +} + +/// Publish the sample listings, marking them verified and compliant so the +/// "trusted" catalogue has content out of the box. +pub fn seed(mkt: &Marketplace) -> Result> { + let mut out = Vec::new(); + for input in sample_listings() { + let listing = mkt.publish(input)?; + mkt.set_verification(&listing.id, VerificationBadge::Verified)?; + let listing = mkt.set_compliance(&listing.id, ComplianceBadge::Compliant)?; + out.push(listing); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn seed_produces_trusted_listings() { + let mkt = Marketplace::in_memory().unwrap(); + let listings = seed(&mkt).unwrap(); + assert_eq!(listings.len(), 2); + assert!(listings.iter().all(|l| l.is_trusted())); + } +} From af6a7e785131436c916f31e106a4690f5ca9ece2 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:13:12 +0400 Subject: [PATCH 11/12] phase 7: add marketplace tests --- backend/controlplane/src/marketplace/store.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/backend/controlplane/src/marketplace/store.rs b/backend/controlplane/src/marketplace/store.rs index 56dcdea..bbe043a 100644 --- a/backend/controlplane/src/marketplace/store.rs +++ b/backend/controlplane/src/marketplace/store.rs @@ -233,4 +233,37 @@ mod tests { assert_eq!(mkt.get(&l.id).unwrap().name, "Permit Intake"); assert!(!l.is_trusted()); // unverified + pending on publish } + + #[test] + fn badges_make_listing_trusted() { + let mkt = Marketplace::in_memory().unwrap(); + let l = mkt.publish(listing()).unwrap(); + mkt.set_verification(&l.id, VerificationBadge::Verified).unwrap(); + let l = mkt.set_compliance(&l.id, ComplianceBadge::Certified).unwrap(); + assert!(l.is_trusted()); + } + + #[test] + fn filters_by_category_and_risk() { + let mkt = Marketplace::in_memory().unwrap(); + mkt.publish(listing()).unwrap(); + assert_eq!(mkt.list().unwrap().len(), 1); + assert_eq!(mkt.list_by_category("licensing").unwrap().len(), 1); + assert_eq!(mkt.list_by_category("nope").unwrap().len(), 0); + assert_eq!(mkt.list_by_risk(RiskLevel::Medium).unwrap().len(), 1); + assert_eq!(mkt.list_by_risk(RiskLevel::Critical).unwrap().len(), 0); + } + + #[test] + fn install_creates_agent_and_counts() { + use crate::registry::AgentRegistry; + let mkt = Marketplace::in_memory().unwrap(); + let reg = AgentRegistry::in_memory().unwrap(); + let l = mkt.publish(listing()).unwrap(); + let agent = mkt.install(&l.id, ®, "Permit Bot A", "team-a", "Licensing").unwrap(); + assert_eq!(agent.name, "Permit Bot A"); + assert_eq!(agent.tools_allowed, vec!["search".to_string()]); + assert_eq!(reg.count().unwrap(), 1); + assert_eq!(mkt.get(&l.id).unwrap().install_count, 1); + } } From 64ba697e073accdc24b2776c7b7c412595da13c2 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 9 Jun 2026 18:13:32 +0400 Subject: [PATCH 12/12] phase 7: add marketplace documentation --- docs/marketplace.md | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/marketplace.md diff --git a/docs/marketplace.md b/docs/marketplace.md new file mode 100644 index 0000000..e7e9fb6 --- /dev/null +++ b/docs/marketplace.md @@ -0,0 +1,67 @@ +# Agent Marketplace + +The marketplace (`clawforge_controlplane::marketplace`) is a **verified, internal +catalogue of reusable agent templates**. Teams publish proven agent blueprints; +other teams install them as governed, registry-tracked agents — without +re-engineering or re-reviewing from scratch. + +## Listings + +A `MarketplaceAgent` listing carries identity (`name`, `description`, +`category`, `department`), social proof (`rating`, `install_count`), a +`risk_level`, two badges, and an `AgentTemplate` blueprint. + +### Badges + +| Badge | Values | Awarded by | +|-------|--------|-----------| +| `verification` | `unverified` → `verified` | platform team | +| `compliance` | `pending` → `compliant` → `certified` | compliance team | + +`is_trusted()` is true only when a listing is **verified** *and* at least +**compliant** — the bar for surfacing it as install-ready. + +## Templates + +An `AgentTemplate` is everything needed to instantiate a concrete agent: +`framework`, default `model_provider`/`model_name`, `required_tools`, +`required_mcp_servers`, `required_model_providers`, `data_access_level`, and +`risk_level`. `to_new_agent(...)` turns it into a registry `NewAgent`. + +## API + +```rust +use clawforge_controlplane::marketplace::{Marketplace, NewListing, AgentTemplate, VerificationBadge, ComplianceBadge}; +use clawforge_controlplane::registry::AgentRegistry; +use clawforge_controlplane::constants::RiskLevel; + +let mkt = Marketplace::open("clawforge-controlplane.db")?; +let reg = AgentRegistry::open("clawforge-controlplane.db")?; + +// Publish (starts unverified + pending) +let listing = mkt.publish(NewListing { /* … template … */ })?; + +// Vet it +mkt.set_verification(&listing.id, VerificationBadge::Verified)?; +mkt.set_compliance(&listing.id, ComplianceBadge::Compliant)?; + +// Browse +let all = mkt.list()?; // most-installed first +let licensing = mkt.list_by_category("licensing")?; +let low_risk = mkt.list_by_risk(RiskLevel::Low)?; + +// Install into the registry (creates a Draft agent, bumps install_count) +let agent = mkt.install(&listing.id, ®, "Permit Bot A", "team-a", "Licensing")?; +``` + +## How it ties together + +`install` is the bridge to the **Agent Registry**: it stamps out a `Draft` +agent from the template, which then flows through the normal governance and +security lifecycle. The marketplace itself stays decoupled from agent +persistence — it is handed the registry to install into. + +## Seed data + +`marketplace::seed::seed(&mkt)` publishes two verified, compliant sample +listings (permit intake, service-desk responder) for demos.