1use super::schema::release_schema;
2use super::types::{
3 AgentReleaseArtifact, AgentReleaseCapability, AgentReleaseCompatibility,
4 AgentReleaseEntrypoint, AgentReleaseHealth, AgentReleaseProvenance,
5 AgentReleaseSecretRequirement, AgentReleaseStorage,
6};
7use super::validation::{
8 parse_artifact, parse_capability, parse_entrypoint, parse_health, parse_provenance,
9 parse_secret, parse_storage, required_block, required_string, unique_capabilities,
10 unique_provenance, unique_secrets, validate_protocol,
11};
12use super::{
13 AgentReleaseError, AgentReleaseField, AGENT_RELEASE_CONTRACT_V1, AGENT_RELEASE_LIMITS,
14};
15use a3s_acl::{
16 canonical_bytes_with_schema, canonical_digest_with_schema, parse_with_limits,
17 validate_document_with_limits,
18};
19use std::io::Read;
20use std::path::Path;
21
22#[derive(Debug, Clone)]
24pub struct AgentReleaseManifest {
25 contract: String,
26 protocol: String,
27 artifact: AgentReleaseArtifact,
28 entrypoint: AgentReleaseEntrypoint,
29 health: AgentReleaseHealth,
30 storage: AgentReleaseStorage,
31 required_capabilities: Vec<AgentReleaseCapability>,
32 required_secrets: Vec<AgentReleaseSecretRequirement>,
33 provenance: Vec<AgentReleaseProvenance>,
34 identity: String,
35 canonical_acl: String,
36}
37
38impl AgentReleaseManifest {
39 pub fn parse(source: &str) -> Result<Self, AgentReleaseError> {
41 let document = parse_with_limits(source, AGENT_RELEASE_LIMITS)?;
42 let schema = release_schema();
43 let report = validate_document_with_limits(&document, &schema, AGENT_RELEASE_LIMITS);
44 if !report.is_empty() {
45 let Some(diagnostic) = report.diagnostics.first() else {
46 return Err(AgentReleaseError::SchemaBudgetExceeded);
47 };
48 return Err(AgentReleaseError::Schema {
49 diagnostic: diagnostic.code,
50 truncated: report.truncated,
51 });
52 }
53
54 let root = required_block(
55 &document.blocks,
56 "agent_release",
57 AgentReleaseField::Contract,
58 )?;
59 let contract = required_string(root, "schema", AgentReleaseField::Contract)?;
60 if contract != AGENT_RELEASE_CONTRACT_V1 {
61 return Err(AgentReleaseError::UnsupportedContract);
62 }
63
64 let protocol = required_string(root, "protocol", AgentReleaseField::Protocol)?;
65 validate_protocol(&protocol)?;
66
67 let artifact = parse_artifact(required_block(
68 &root.blocks,
69 "artifact",
70 AgentReleaseField::ArtifactDigest,
71 )?)?;
72 let entrypoint = parse_entrypoint(required_block(
73 &root.blocks,
74 "entrypoint",
75 AgentReleaseField::EntrypointCommand,
76 )?)?;
77 let health = parse_health(required_block(
78 &root.blocks,
79 "health",
80 AgentReleaseField::HealthTransport,
81 )?)?;
82 let storage = parse_storage(required_block(
83 &root.blocks,
84 "storage",
85 AgentReleaseField::WorkspaceMode,
86 )?)?;
87
88 let required_capabilities = unique_capabilities(
89 root.blocks
90 .iter()
91 .filter(|block| block.name == "capability")
92 .map(parse_capability)
93 .collect::<Result<Vec<_>, _>>()?,
94 )?;
95 let required_secrets = unique_secrets(
96 root.blocks
97 .iter()
98 .filter(|block| block.name == "secret")
99 .map(parse_secret)
100 .collect::<Result<Vec<_>, _>>()?,
101 )?;
102 if !required_secrets.is_empty()
103 && !required_capabilities
104 .iter()
105 .any(|capability| capability.name == "secrets.external")
106 {
107 return Err(AgentReleaseError::InvalidField(
108 AgentReleaseField::CapabilityName,
109 ));
110 }
111 let provenance = unique_provenance(
112 root.blocks
113 .iter()
114 .filter(|block| block.name == "provenance")
115 .map(parse_provenance)
116 .collect::<Result<Vec<_>, _>>()?,
117 )?;
118
119 let canonical_acl = String::from_utf8(canonical_bytes_with_schema(&document, &schema)?)
120 .map_err(|_| AgentReleaseError::CanonicalEncoding)?;
121 let identity = canonical_digest_with_schema(&document, &schema)?;
122
123 Ok(Self {
124 contract,
125 protocol,
126 artifact,
127 entrypoint,
128 health,
129 storage,
130 required_capabilities,
131 required_secrets,
132 provenance,
133 identity,
134 canonical_acl,
135 })
136 }
137
138 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, AgentReleaseError> {
140 let file = std::fs::File::open(path)?;
141 let mut bytes = Vec::new();
142 file.take(AGENT_RELEASE_LIMITS.max_document_bytes as u64 + 1)
143 .read_to_end(&mut bytes)?;
144 if bytes.len() > AGENT_RELEASE_LIMITS.max_document_bytes {
145 return Err(AgentReleaseError::InputTooLarge);
146 }
147 let source = std::str::from_utf8(&bytes).map_err(|_| AgentReleaseError::InvalidEncoding)?;
148 Self::parse(source)
149 }
150
151 pub fn contract(&self) -> &str {
152 &self.contract
153 }
154
155 pub fn protocol(&self) -> &str {
156 &self.protocol
157 }
158
159 pub fn artifact(&self) -> &AgentReleaseArtifact {
160 &self.artifact
161 }
162
163 pub fn entrypoint(&self) -> &AgentReleaseEntrypoint {
164 &self.entrypoint
165 }
166
167 pub fn health(&self) -> &AgentReleaseHealth {
168 &self.health
169 }
170
171 pub fn storage(&self) -> &AgentReleaseStorage {
172 &self.storage
173 }
174
175 pub fn required_capabilities(&self) -> &[AgentReleaseCapability] {
176 &self.required_capabilities
177 }
178
179 pub fn required_secrets(&self) -> &[AgentReleaseSecretRequirement] {
180 &self.required_secrets
181 }
182
183 pub fn provenance(&self) -> &[AgentReleaseProvenance] {
184 &self.provenance
185 }
186
187 pub fn identity(&self) -> &str {
189 &self.identity
190 }
191
192 pub fn canonical_acl(&self) -> &str {
194 &self.canonical_acl
195 }
196
197 pub fn verify_compatibility(
199 &self,
200 available: &AgentReleaseCompatibility,
201 ) -> Result<(), AgentReleaseError> {
202 if self.protocol != available.protocol {
203 return Err(AgentReleaseError::IncompatibleProtocol);
204 }
205 for (required_index, required) in self.required_capabilities.iter().enumerate() {
206 let supported = available
207 .capabilities
208 .iter()
209 .find(|capability| capability.name == required.name);
210 if supported.is_none_or(|capability| capability.level < required.level) {
211 return Err(AgentReleaseError::UnsupportedCapability { required_index });
212 }
213 }
214 Ok(())
215 }
216}