use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::identity::{verify_signature, AgentIdentity};
use crate::mcp::McpServerConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Capability {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
pub config: Option<serde_json::Value>,
}
fn default_true() -> bool {
true
}
impl Capability {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
enabled: true,
config: None,
}
}
#[must_use]
pub fn with_config(mut self, config: serde_json::Value) -> Self {
self.config = Some(config);
self
}
#[must_use]
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct AgentManifest {
pub name: String,
pub version: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub capabilities: Vec<Capability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(default)]
pub skills: Vec<String>,
#[serde(default)]
pub mcp_servers: Vec<McpServerConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub did: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub key_history: Vec<crate::trust::RotationProof>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
impl AgentManifest {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
..Default::default()
}
}
pub fn from_json(json: &str) -> Result<Self> {
Ok(serde_json::from_str(json)?)
}
pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self> {
let contents = std::fs::read_to_string(path)?;
Self::from_json(&contents)
}
pub fn to_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
pub fn to_json_file(&self, path: impl AsRef<Path>) -> Result<()> {
std::fs::write(path, self.to_json()?)?;
Ok(())
}
pub fn signable_bytes(&self) -> Result<Vec<u8>> {
let mut unsigned = self.clone();
unsigned.signature = None;
Ok(serde_json::to_vec(&unsigned)?)
}
pub fn sign(&mut self, identity: &AgentIdentity) -> Result<()> {
self.public_key = Some(identity.public_key_base64());
self.did = Some(identity.did_key());
let bytes = self.signable_bytes()?;
self.signature = Some(identity.sign(&bytes));
Ok(())
}
pub fn rotate_identity(&mut self, old: &AgentIdentity, new: &AgentIdentity) -> Result<()> {
if self.public_key.as_deref() != Some(old.public_key_base64().as_str()) {
return Err(Error::Identity(
"rotation must start from the manifest's current key".into(),
));
}
self.key_history
.push(crate::trust::RotationProof::create(old, new));
self.sign(new)
}
pub fn verify(&self) -> Result<()> {
let public_key = self
.public_key
.as_deref()
.ok_or_else(|| Error::Verification("manifest has no public key".into()))?;
if let Some(did) = &self.did {
if crate::identity::public_key_from_did(did)? != public_key {
return Err(Error::Verification(
"manifest did does not match its public key".into(),
));
}
}
self.verify_with(public_key)
}
pub fn verify_with(&self, key_or_did: &str) -> Result<()> {
let signature = self
.signature
.as_deref()
.ok_or_else(|| Error::Verification("manifest is not signed".into()))?;
verify_signature(key_or_did, &self.signable_bytes()?, signature)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> AgentManifest {
let mut m = AgentManifest::new("test-agent", "1.2.3");
m.description = "A test agent".into();
m.capabilities
.push(Capability::new("chat", "Talks").with_config(serde_json::json!({"k": 1})));
m.skills.push("echo".into());
m
}
#[test]
fn json_roundtrip() {
let manifest = sample();
let json = manifest.to_json().unwrap();
let parsed = AgentManifest::from_json(&json).unwrap();
assert_eq!(manifest, parsed);
}
#[test]
fn sign_then_verify() {
let identity = AgentIdentity::generate();
let mut manifest = sample();
manifest.sign(&identity).unwrap();
manifest.verify().unwrap();
manifest.verify_with(&identity.public_key_base64()).unwrap();
}
#[test]
fn tampering_breaks_verification() {
let identity = AgentIdentity::generate();
let mut manifest = sample();
manifest.sign(&identity).unwrap();
manifest.version = "9.9.9".into();
assert!(manifest.verify().is_err());
}
#[test]
fn verification_survives_json_roundtrip() {
let identity = AgentIdentity::generate();
let mut manifest = sample();
manifest.sign(&identity).unwrap();
let reparsed = AgentManifest::from_json(&manifest.to_json().unwrap()).unwrap();
reparsed.verify().unwrap();
}
#[test]
fn unsigned_manifest_fails_verification() {
assert!(sample().verify().is_err());
}
#[test]
fn capability_defaults_enabled() {
let cap: Capability = serde_json::from_str(r#"{"name": "chat"}"#).unwrap();
assert!(cap.enabled);
assert!(cap.description.is_empty());
}
#[test]
fn signing_embeds_matching_did() {
let identity = AgentIdentity::generate();
let mut manifest = sample();
manifest.sign(&identity).unwrap();
assert_eq!(manifest.did, Some(identity.did_key()));
manifest.verify().unwrap();
manifest.verify_with(&identity.did_key()).unwrap();
manifest.did = Some(AgentIdentity::generate().did_key());
assert!(manifest.verify().is_err());
}
#[test]
fn rotate_identity_re_signs_and_records_history() {
let old = AgentIdentity::generate();
let new = AgentIdentity::generate();
let mut manifest = sample();
manifest.sign(&old).unwrap();
manifest.rotate_identity(&old, &new).unwrap();
assert_eq!(manifest.public_key, Some(new.public_key_base64()));
assert_eq!(manifest.key_history.len(), 1);
manifest.verify().unwrap();
manifest.key_history[0].verify().unwrap();
let stranger = AgentIdentity::generate();
assert!(manifest.rotate_identity(&stranger, &old).is_err());
}
}