use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{UseError, UseResult};
use super::validation::strictly_sorted_unique;
use super::{canonical_digest, canonical_json, contract_error, parse_contract};
pub const CAPABILITY_CONSUMER_PROFILE_SCHEMA_V1: &str = "a3s.use.capability-consumer-profile.v1";
pub const CAPABILITY_CONSUMER_NEGOTIATION_SCHEMA_V1: &str =
"a3s.use.capability-consumer-negotiation.v1";
const PROFILE_ERROR: &str = "use.plugin.capability_consumer_profile_invalid";
const NEGOTIATION_ERROR: &str = "use.plugin.capability_consumer_negotiation_invalid";
pub const MAX_CAPABILITY_CONSUMER_EXTENSIONS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityConsumerKind {
GenericMcp,
A3s,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CapabilityConsumerExtension {
Flow,
Knowledge,
Ui,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CapabilityConsumerProfile {
pub schema: String,
pub kind: CapabilityConsumerKind,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_extensions: Vec<CapabilityConsumerExtension>,
}
impl CapabilityConsumerProfile {
pub fn generic_mcp() -> Self {
Self {
schema: CAPABILITY_CONSUMER_PROFILE_SCHEMA_V1.to_owned(),
kind: CapabilityConsumerKind::GenericMcp,
requested_extensions: Vec::new(),
}
}
pub fn a3s(
extensions: impl IntoIterator<Item = CapabilityConsumerExtension>,
) -> UseResult<Self> {
let profile = Self {
schema: CAPABILITY_CONSUMER_PROFILE_SCHEMA_V1.to_owned(),
kind: CapabilityConsumerKind::A3s,
requested_extensions: sorted_extensions(extensions)?,
};
profile.validate()?;
Ok(profile)
}
pub fn from_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"capability consumer profile",
PROFILE_ERROR,
Self::validate,
)
}
pub fn validate(&self) -> UseResult<()> {
if self.schema != CAPABILITY_CONSUMER_PROFILE_SCHEMA_V1
|| self.requested_extensions.len() > MAX_CAPABILITY_CONSUMER_EXTENSIONS
|| !strictly_sorted_unique(&self.requested_extensions)
|| (self.kind == CapabilityConsumerKind::GenericMcp
&& !self.requested_extensions.is_empty())
{
return Err(profile_error(
"The capability consumer profile schema, ordering, or extension policy is invalid.",
));
}
Ok(())
}
pub fn canonical_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
canonical_json(self, "capability consumer profile", PROFILE_ERROR)
}
pub fn descriptor_digest(&self) -> UseResult<String> {
Ok(canonical_digest(&self.canonical_bytes()?))
}
pub fn kind(&self) -> CapabilityConsumerKind {
self.kind
}
pub fn requested_extensions(&self) -> &[CapabilityConsumerExtension] {
&self.requested_extensions
}
pub fn is_generic_mcp(&self) -> bool {
self.kind == CapabilityConsumerKind::GenericMcp
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CapabilityConsumerNegotiation {
pub schema: String,
pub profile: CapabilityConsumerProfile,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub accepted_extensions: Vec<CapabilityConsumerExtension>,
}
impl CapabilityConsumerNegotiation {
pub fn generic_mcp() -> Self {
Self {
schema: CAPABILITY_CONSUMER_NEGOTIATION_SCHEMA_V1.to_owned(),
profile: CapabilityConsumerProfile::generic_mcp(),
accepted_extensions: Vec::new(),
}
}
pub fn negotiate(
profile: CapabilityConsumerProfile,
supported_extensions: impl IntoIterator<Item = CapabilityConsumerExtension>,
) -> UseResult<Self> {
profile.validate()?;
let supported = sorted_extensions(supported_extensions)?;
if profile
.requested_extensions
.iter()
.any(|extension| !supported.contains(extension))
{
return Err(negotiation_error(
"The host does not support every requested consumer extension.",
));
}
let negotiation = Self {
schema: CAPABILITY_CONSUMER_NEGOTIATION_SCHEMA_V1.to_owned(),
accepted_extensions: profile.requested_extensions.clone(),
profile,
};
negotiation.validate()?;
Ok(negotiation)
}
pub fn from_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"capability consumer negotiation",
NEGOTIATION_ERROR,
Self::validate,
)
}
pub fn validate(&self) -> UseResult<()> {
self.profile.validate()?;
if self.schema != CAPABILITY_CONSUMER_NEGOTIATION_SCHEMA_V1
|| self.accepted_extensions.len() > MAX_CAPABILITY_CONSUMER_EXTENSIONS
|| !strictly_sorted_unique(&self.accepted_extensions)
|| self.accepted_extensions != self.profile.requested_extensions
{
return Err(negotiation_error(
"The capability consumer negotiation schema or accepted set is invalid.",
));
}
Ok(())
}
pub fn canonical_bytes(&self) -> UseResult<Vec<u8>> {
self.validate()?;
canonical_json(self, "capability consumer negotiation", NEGOTIATION_ERROR)
}
pub fn descriptor_digest(&self) -> UseResult<String> {
Ok(canonical_digest(&self.canonical_bytes()?))
}
pub fn profile(&self) -> &CapabilityConsumerProfile {
&self.profile
}
pub fn accepted_extensions(&self) -> &[CapabilityConsumerExtension] {
&self.accepted_extensions
}
pub fn accepts(&self, extension: CapabilityConsumerExtension) -> bool {
self.accepted_extensions.contains(&extension)
}
}
fn sorted_extensions(
extensions: impl IntoIterator<Item = CapabilityConsumerExtension>,
) -> UseResult<Vec<CapabilityConsumerExtension>> {
let mut values = extensions.into_iter().collect::<Vec<_>>();
if values.len() > MAX_CAPABILITY_CONSUMER_EXTENSIONS {
return Err(profile_error(
"The capability consumer extension set exceeds its bound.",
));
}
values.sort();
let mut unique = BTreeSet::new();
if values.iter().any(|value| !unique.insert(*value)) {
return Err(profile_error(
"The capability consumer extension set contains duplicates.",
));
}
Ok(values)
}
fn profile_error(message: impl Into<String>) -> UseError {
contract_error(PROFILE_ERROR, message)
}
fn negotiation_error(message: impl Into<String>) -> UseError {
contract_error(NEGOTIATION_ERROR, message)
}
#[cfg(test)]
#[path = "capability_consumer_tests.rs"]
mod tests;