use super::schema::release_schema;
use super::types::{
AgentReleaseArtifact, AgentReleaseCapability, AgentReleaseCompatibility,
AgentReleaseEntrypoint, AgentReleaseHealth, AgentReleaseProvenance,
AgentReleaseSecretRequirement, AgentReleaseStorage,
};
use super::validation::{
parse_artifact, parse_capability, parse_entrypoint, parse_health, parse_provenance,
parse_secret, parse_storage, required_block, required_string, unique_capabilities,
unique_provenance, unique_secrets, validate_protocol,
};
use super::{
AgentReleaseError, AgentReleaseField, AGENT_RELEASE_CONTRACT_V1, AGENT_RELEASE_LIMITS,
};
use a3s_acl::{
canonical_bytes_with_schema, canonical_digest_with_schema, parse_with_limits,
validate_document_with_limits,
};
use std::io::Read;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct AgentReleaseManifest {
contract: String,
protocol: String,
artifact: AgentReleaseArtifact,
entrypoint: AgentReleaseEntrypoint,
health: AgentReleaseHealth,
storage: AgentReleaseStorage,
required_capabilities: Vec<AgentReleaseCapability>,
required_secrets: Vec<AgentReleaseSecretRequirement>,
provenance: Vec<AgentReleaseProvenance>,
identity: String,
canonical_acl: String,
}
impl AgentReleaseManifest {
pub fn parse(source: &str) -> Result<Self, AgentReleaseError> {
let document = parse_with_limits(source, AGENT_RELEASE_LIMITS)?;
let schema = release_schema();
let report = validate_document_with_limits(&document, &schema, AGENT_RELEASE_LIMITS);
if !report.is_empty() {
let Some(diagnostic) = report.diagnostics.first() else {
return Err(AgentReleaseError::SchemaBudgetExceeded);
};
return Err(AgentReleaseError::Schema {
diagnostic: diagnostic.code,
truncated: report.truncated,
});
}
let root = required_block(
&document.blocks,
"agent_release",
AgentReleaseField::Contract,
)?;
let contract = required_string(root, "schema", AgentReleaseField::Contract)?;
if contract != AGENT_RELEASE_CONTRACT_V1 {
return Err(AgentReleaseError::UnsupportedContract);
}
let protocol = required_string(root, "protocol", AgentReleaseField::Protocol)?;
validate_protocol(&protocol)?;
let artifact = parse_artifact(required_block(
&root.blocks,
"artifact",
AgentReleaseField::ArtifactDigest,
)?)?;
let entrypoint = parse_entrypoint(required_block(
&root.blocks,
"entrypoint",
AgentReleaseField::EntrypointCommand,
)?)?;
let health = parse_health(required_block(
&root.blocks,
"health",
AgentReleaseField::HealthTransport,
)?)?;
let storage = parse_storage(required_block(
&root.blocks,
"storage",
AgentReleaseField::WorkspaceMode,
)?)?;
let required_capabilities = unique_capabilities(
root.blocks
.iter()
.filter(|block| block.name == "capability")
.map(parse_capability)
.collect::<Result<Vec<_>, _>>()?,
)?;
let required_secrets = unique_secrets(
root.blocks
.iter()
.filter(|block| block.name == "secret")
.map(parse_secret)
.collect::<Result<Vec<_>, _>>()?,
)?;
if !required_secrets.is_empty()
&& !required_capabilities
.iter()
.any(|capability| capability.name == "secrets.external")
{
return Err(AgentReleaseError::InvalidField(
AgentReleaseField::CapabilityName,
));
}
let provenance = unique_provenance(
root.blocks
.iter()
.filter(|block| block.name == "provenance")
.map(parse_provenance)
.collect::<Result<Vec<_>, _>>()?,
)?;
let canonical_acl = String::from_utf8(canonical_bytes_with_schema(&document, &schema)?)
.map_err(|_| AgentReleaseError::CanonicalEncoding)?;
let identity = canonical_digest_with_schema(&document, &schema)?;
Ok(Self {
contract,
protocol,
artifact,
entrypoint,
health,
storage,
required_capabilities,
required_secrets,
provenance,
identity,
canonical_acl,
})
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, AgentReleaseError> {
let file = std::fs::File::open(path)?;
let mut bytes = Vec::new();
file.take(AGENT_RELEASE_LIMITS.max_document_bytes as u64 + 1)
.read_to_end(&mut bytes)?;
if bytes.len() > AGENT_RELEASE_LIMITS.max_document_bytes {
return Err(AgentReleaseError::InputTooLarge);
}
let source = std::str::from_utf8(&bytes).map_err(|_| AgentReleaseError::InvalidEncoding)?;
Self::parse(source)
}
pub fn contract(&self) -> &str {
&self.contract
}
pub fn protocol(&self) -> &str {
&self.protocol
}
pub fn artifact(&self) -> &AgentReleaseArtifact {
&self.artifact
}
pub fn entrypoint(&self) -> &AgentReleaseEntrypoint {
&self.entrypoint
}
pub fn health(&self) -> &AgentReleaseHealth {
&self.health
}
pub fn storage(&self) -> &AgentReleaseStorage {
&self.storage
}
pub fn required_capabilities(&self) -> &[AgentReleaseCapability] {
&self.required_capabilities
}
pub fn required_secrets(&self) -> &[AgentReleaseSecretRequirement] {
&self.required_secrets
}
pub fn provenance(&self) -> &[AgentReleaseProvenance] {
&self.provenance
}
pub fn identity(&self) -> &str {
&self.identity
}
pub fn canonical_acl(&self) -> &str {
&self.canonical_acl
}
pub fn verify_compatibility(
&self,
available: &AgentReleaseCompatibility,
) -> Result<(), AgentReleaseError> {
if self.protocol != available.protocol {
return Err(AgentReleaseError::IncompatibleProtocol);
}
for (required_index, required) in self.required_capabilities.iter().enumerate() {
let supported = available
.capabilities
.iter()
.find(|capability| capability.name == required.name);
if supported.is_none_or(|capability| capability.level < required.level) {
return Err(AgentReleaseError::UnsupportedCapability { required_index });
}
}
Ok(())
}
}