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_digest, 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, Value,
18};
19use std::collections::BTreeMap;
20use std::io::Read;
21use std::path::Path;
22
23#[derive(Debug, Clone)]
25pub struct AgentReleaseManifest {
26 contract: String,
27 protocol: String,
28 artifact: AgentReleaseArtifact,
29 entrypoint: AgentReleaseEntrypoint,
30 health: AgentReleaseHealth,
31 storage: AgentReleaseStorage,
32 required_capabilities: Vec<AgentReleaseCapability>,
33 required_secrets: Vec<AgentReleaseSecretRequirement>,
34 provenance: Vec<AgentReleaseProvenance>,
35 identity: String,
36 canonical_acl: String,
37}
38
39impl AgentReleaseManifest {
40 pub fn parse(source: &str) -> Result<Self, AgentReleaseError> {
42 let document = parse_with_limits(source, AGENT_RELEASE_LIMITS)?;
43 let schema = release_schema();
44 let report = validate_document_with_limits(&document, &schema, AGENT_RELEASE_LIMITS);
45 if !report.is_empty() {
46 let Some(diagnostic) = report.diagnostics.first() else {
47 return Err(AgentReleaseError::SchemaBudgetExceeded);
48 };
49 return Err(AgentReleaseError::Schema {
50 diagnostic: diagnostic.code,
51 truncated: report.truncated,
52 });
53 }
54
55 let root = required_block(
56 &document.blocks,
57 "agent_release",
58 AgentReleaseField::Contract,
59 )?;
60 let contract = required_string(root, "schema", AgentReleaseField::Contract)?;
61 if contract != AGENT_RELEASE_CONTRACT_V1 {
62 return Err(AgentReleaseError::UnsupportedContract);
63 }
64
65 let protocol = required_string(root, "protocol", AgentReleaseField::Protocol)?;
66 validate_protocol(&protocol)?;
67
68 let artifact = parse_artifact(required_block(
69 &root.blocks,
70 "artifact",
71 AgentReleaseField::ArtifactDigest,
72 )?)?;
73 let entrypoint = parse_entrypoint(required_block(
74 &root.blocks,
75 "entrypoint",
76 AgentReleaseField::EntrypointCommand,
77 )?)?;
78 let health = parse_health(required_block(
79 &root.blocks,
80 "health",
81 AgentReleaseField::HealthTransport,
82 )?)?;
83 let storage = parse_storage(required_block(
84 &root.blocks,
85 "storage",
86 AgentReleaseField::WorkspaceMode,
87 )?)?;
88
89 let required_capabilities = unique_capabilities(
90 root.blocks
91 .iter()
92 .filter(|block| block.name == "capability")
93 .map(parse_capability)
94 .collect::<Result<Vec<_>, _>>()?,
95 )?;
96 let required_secrets = unique_secrets(
97 root.blocks
98 .iter()
99 .filter(|block| block.name == "secret")
100 .map(parse_secret)
101 .collect::<Result<Vec<_>, _>>()?,
102 )?;
103 if !required_secrets.is_empty()
104 && !required_capabilities
105 .iter()
106 .any(|capability| capability.name == "secrets.external")
107 {
108 return Err(AgentReleaseError::InvalidField(
109 AgentReleaseField::CapabilityName,
110 ));
111 }
112 let provenance = unique_provenance(
113 root.blocks
114 .iter()
115 .filter(|block| block.name == "provenance")
116 .map(parse_provenance)
117 .collect::<Result<Vec<_>, _>>()?,
118 )?;
119
120 let canonical_acl = String::from_utf8(canonical_bytes_with_schema(&document, &schema)?)
121 .map_err(|_| AgentReleaseError::CanonicalEncoding)?;
122 let identity = canonical_digest_with_schema(&document, &schema)?;
123
124 Ok(Self {
125 contract,
126 protocol,
127 artifact,
128 entrypoint,
129 health,
130 storage,
131 required_capabilities,
132 required_secrets,
133 provenance,
134 identity,
135 canonical_acl,
136 })
137 }
138
139 pub fn from_file(path: impl AsRef<Path>) -> Result<Self, AgentReleaseError> {
141 let file = std::fs::File::open(path)?;
142 let mut bytes = Vec::new();
143 file.take(AGENT_RELEASE_LIMITS.max_document_bytes as u64 + 1)
144 .read_to_end(&mut bytes)?;
145 if bytes.len() > AGENT_RELEASE_LIMITS.max_document_bytes {
146 return Err(AgentReleaseError::InputTooLarge);
147 }
148 let source = std::str::from_utf8(&bytes).map_err(|_| AgentReleaseError::InvalidEncoding)?;
149 Self::parse(source)
150 }
151
152 pub fn contract(&self) -> &str {
153 &self.contract
154 }
155
156 pub fn protocol(&self) -> &str {
157 &self.protocol
158 }
159
160 pub fn artifact(&self) -> &AgentReleaseArtifact {
161 &self.artifact
162 }
163
164 pub fn entrypoint(&self) -> &AgentReleaseEntrypoint {
165 &self.entrypoint
166 }
167
168 pub fn health(&self) -> &AgentReleaseHealth {
169 &self.health
170 }
171
172 pub fn storage(&self) -> &AgentReleaseStorage {
173 &self.storage
174 }
175
176 pub fn required_capabilities(&self) -> &[AgentReleaseCapability] {
177 &self.required_capabilities
178 }
179
180 pub fn required_secrets(&self) -> &[AgentReleaseSecretRequirement] {
181 &self.required_secrets
182 }
183
184 pub fn provenance(&self) -> &[AgentReleaseProvenance] {
185 &self.provenance
186 }
187
188 pub fn identity(&self) -> &str {
190 &self.identity
191 }
192
193 pub fn canonical_acl(&self) -> &str {
195 &self.canonical_acl
196 }
197
198 pub fn bind_publication(
207 &self,
208 artifact_digest: impl Into<String>,
209 provenance: impl IntoIterator<Item = AgentReleaseProvenance>,
210 ) -> Result<Self, AgentReleaseError> {
211 let artifact_digest = artifact_digest.into();
212 validate_digest(&artifact_digest, AgentReleaseField::ArtifactDigest)?;
213 let provenance = unique_provenance(provenance)?
214 .into_iter()
215 .map(|reference| (reference.kind.clone(), reference))
216 .collect::<BTreeMap<_, _>>();
217 if provenance.len() != self.provenance.len()
218 || self
219 .provenance
220 .iter()
221 .any(|reference| !provenance.contains_key(reference.kind()))
222 {
223 return Err(AgentReleaseError::InvalidField(
224 AgentReleaseField::ProvenanceKind,
225 ));
226 }
227
228 let mut document = parse_with_limits(&self.canonical_acl, AGENT_RELEASE_LIMITS)?;
229 let root = document
230 .blocks
231 .iter_mut()
232 .find(|block| block.name == "agent_release")
233 .ok_or(AgentReleaseError::InvalidField(AgentReleaseField::Contract))?;
234 let artifact = root
235 .blocks
236 .iter_mut()
237 .find(|block| block.name == "artifact")
238 .ok_or(AgentReleaseError::InvalidField(
239 AgentReleaseField::ArtifactDigest,
240 ))?;
241 artifact
242 .attributes
243 .insert("digest".into(), Value::String(artifact_digest));
244
245 for block in root
246 .blocks
247 .iter_mut()
248 .filter(|block| block.name == "provenance")
249 {
250 let kind = block.labels.first().ok_or(AgentReleaseError::InvalidField(
251 AgentReleaseField::ProvenanceKind,
252 ))?;
253 let reference = provenance.get(kind).ok_or(AgentReleaseError::InvalidField(
254 AgentReleaseField::ProvenanceKind,
255 ))?;
256 block
257 .attributes
258 .insert("uri".into(), Value::String(reference.uri.clone()));
259 block
260 .attributes
261 .insert("digest".into(), Value::String(reference.digest.clone()));
262 }
263
264 let canonical =
265 String::from_utf8(canonical_bytes_with_schema(&document, &release_schema())?)
266 .map_err(|_| AgentReleaseError::CanonicalEncoding)?;
267 Self::parse(&canonical)
268 }
269
270 pub fn verify_compatibility(
272 &self,
273 available: &AgentReleaseCompatibility,
274 ) -> Result<(), AgentReleaseError> {
275 if self.protocol != available.protocol {
276 return Err(AgentReleaseError::IncompatibleProtocol);
277 }
278 for (required_index, required) in self.required_capabilities.iter().enumerate() {
279 let supported = available
280 .capabilities
281 .iter()
282 .find(|capability| capability.name == required.name);
283 if supported.is_none_or(|capability| capability.level < required.level) {
284 return Err(AgentReleaseError::UnsupportedCapability { required_index });
285 }
286 }
287 Ok(())
288 }
289}