Skip to main content

a3s_code_core/release/
manifest.rs

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/// Admitted, canonical Agent release manifest.
23#[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    /// Parse, admit, and canonicalize one untrusted `.a3s/asset.acl` document.
40    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    /// Read at most the manifest budget plus one byte before parsing.
139    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    /// Lowercase SHA-256 digest of the schema-aware canonical ACL bytes.
188    pub fn identity(&self) -> &str {
189        &self.identity
190    }
191
192    /// Schema-aware canonical ACL bytes as UTF-8 with one final newline.
193    pub fn canonical_acl(&self) -> &str {
194        &self.canonical_acl
195    }
196
197    /// Fail before activation unless protocol and every required capability match.
198    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}