use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::Platform;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(rename = "filesystem.plugins.Manifest")]
pub struct Manifest {
pub description: String,
pub version: String,
pub owner: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub author: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub homepage: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub license: Option<String>,
#[serde(default, skip_serializing_if = "Binaries::is_empty")]
#[schemars(extend("omitempty" = true))]
pub binaries: Binaries,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub viewer_zip: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub viewer_url: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub viewer_routes: Vec<ViewerRoute>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub mobile_ready: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[schemars(extend("omitempty" = true))]
pub mcp_servers: Vec<McpServer>,
}
impl Manifest {
pub fn has_viewer(&self) -> bool {
self.viewer_zip.is_some() || self.viewer_url.is_some()
}
pub fn tool_name(&self, name: &str) -> String {
objectiveai_sdk::agent::materialize_tool_name(&self.owner, name, &self.version)
}
pub fn validate(&self) -> Result<(), &'static str> {
if self.viewer_zip.is_some() && self.viewer_url.is_some() {
return Err("viewer_zip and viewer_url are mutually exclusive");
}
if let Some(url) = self.viewer_url.as_deref() {
validate_viewer_url(url)?;
}
for entry in &self.mcp_servers {
if entry.name.is_empty() {
return Err("mcp_servers[i].name cannot be empty");
}
if entry.url.is_empty() {
return Err("mcp_servers[i].url cannot be empty");
}
}
for (i, a) in self.mcp_servers.iter().enumerate() {
for b in &self.mcp_servers[i + 1..] {
if a.name == b.name {
return Err("mcp_servers contains duplicate name");
}
if a.url == b.url {
return Err("mcp_servers contains duplicate url");
}
}
}
Ok(())
}
}
fn validate_viewer_url(url: &str) -> Result<(), &'static str> {
let url = url.trim();
if url.is_empty() {
return Err("viewer_url cannot be empty");
}
if url.starts_with("https://") {
return Ok(());
}
if let Some(rest) = url.strip_prefix("http://") {
let host_end = rest
.find(|c: char| matches!(c, '/' | ':' | '?' | '#'))
.unwrap_or(rest.len());
let host = &rest[..host_end];
if host == "localhost" || host == "127.0.0.1" {
return Ok(());
}
return Err(
"viewer_url with http:// scheme is only allowed for localhost or 127.0.0.1",
);
}
Err("viewer_url must use https:// or http://localhost / http://127.0.0.1")
}
#[derive(
Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq,
)]
#[schemars(rename = "filesystem.plugins.Binaries")]
pub struct Binaries {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub linux_x86_64: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub linux_aarch64: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub windows_x86_64: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub windows_aarch64: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub macos_x86_64: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("omitempty" = true))]
pub macos_aarch64: Option<String>,
}
impl Binaries {
pub fn get(&self, platform: Platform) -> Option<&String> {
match platform {
Platform::LinuxX86_64 => self.linux_x86_64.as_ref(),
Platform::LinuxAarch64 => self.linux_aarch64.as_ref(),
Platform::WindowsX86_64 => self.windows_x86_64.as_ref(),
Platform::WindowsAarch64 => self.windows_aarch64.as_ref(),
Platform::MacosX86_64 => self.macos_x86_64.as_ref(),
Platform::MacosAarch64 => self.macos_aarch64.as_ref(),
}
}
pub fn is_empty(&self) -> bool {
self.linux_x86_64.is_none()
&& self.linux_aarch64.is_none()
&& self.windows_x86_64.is_none()
&& self.windows_aarch64.is_none()
&& self.macos_x86_64.is_none()
&& self.macos_aarch64.is_none()
}
pub fn len(&self) -> usize {
[
&self.linux_x86_64,
&self.linux_aarch64,
&self.windows_x86_64,
&self.windows_aarch64,
&self.macos_x86_64,
&self.macos_aarch64,
]
.iter()
.filter(|o| o.is_some())
.count()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[schemars(rename = "filesystem.plugins.McpServer")]
pub struct McpServer {
pub name: String,
pub url: String,
#[serde(default)]
pub authorization: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(rename = "filesystem.plugins.ViewerRoute")]
pub struct ViewerRoute {
pub path: String,
pub method: HttpMethod,
#[serde(rename = "type")]
pub r#type: String,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema,
)]
#[schemars(rename = "filesystem.plugins.HttpMethod")]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(rename = "filesystem.plugins.ManifestWithNameAndSource")]
pub struct ManifestWithNameAndSource {
pub name: String,
#[serde(flatten)]
pub manifest: Manifest,
pub source: String,
}
impl ManifestWithNameAndSource {
pub fn tool_name(&self) -> String {
self.manifest.tool_name(&self.name)
}
}
impl From<ManifestWithNameAndSource>
for objectiveai_sdk::cli::command::plugins::get::ResponseManifest
{
fn from(m: ManifestWithNameAndSource) -> Self {
use objectiveai_sdk::cli::command::plugins::get::{
ResponseBinaries, ResponseHttpMethod, ResponseManifest, ResponseMcpServer,
ResponseViewerRoute,
};
let manifest = m.manifest;
ResponseManifest {
name: m.name,
description: manifest.description,
version: manifest.version,
owner: manifest.owner,
author: manifest.author,
homepage: manifest.homepage,
license: manifest.license,
binaries: ResponseBinaries {
linux_x86_64: manifest.binaries.linux_x86_64,
linux_aarch64: manifest.binaries.linux_aarch64,
windows_x86_64: manifest.binaries.windows_x86_64,
windows_aarch64: manifest.binaries.windows_aarch64,
macos_x86_64: manifest.binaries.macos_x86_64,
macos_aarch64: manifest.binaries.macos_aarch64,
},
viewer_zip: manifest.viewer_zip,
viewer_url: manifest.viewer_url,
viewer_routes: manifest
.viewer_routes
.into_iter()
.map(|r| ResponseViewerRoute {
path: r.path,
method: match r.method {
HttpMethod::Get => ResponseHttpMethod::Get,
HttpMethod::Post => ResponseHttpMethod::Post,
HttpMethod::Put => ResponseHttpMethod::Put,
HttpMethod::Patch => ResponseHttpMethod::Patch,
HttpMethod::Delete => ResponseHttpMethod::Delete,
},
r#type: r.r#type,
})
.collect(),
mobile_ready: manifest.mobile_ready,
mcp_servers: manifest
.mcp_servers
.into_iter()
.map(|s| ResponseMcpServer {
name: s.name,
url: s.url,
authorization: s.authorization,
})
.collect(),
source: m.source,
}
}
}