aion_server/update_check/document.rs
1//! The embedded update-check document: its bytes, its compiled identity, and
2//! the verified names the server half keys on.
3//!
4//! # One document, one place
5//!
6//! `crates/aion-server/update-check-embed/update-check.awl` is the ONLY copy
7//! of the update-check document in this repository, embedded exactly as the
8//! assistant's is (`include_str!` from inside the crate, so `cargo package`
9//! carries it and an installed binary holds the same bytes this repository
10//! does). The boot install, the dispatch observer, and this module's tests
11//! all read this one artifact.
12//!
13//! # The names are declared once and VERIFIED, never restated
14//!
15//! The dispatch observer ([`super::observer`]) recognises the check by its
16//! task queue and action name, and trusts a completed dispatch only when the
17//! run's resolved body is the exact command below. Those three names are
18//! declared here as constants and [`EmbeddedUpdateCheck::load`] proves each
19//! one against the compiled document before returning — a document edit that
20//! renames the queue, the action, or the command fails loudly at load (at
21//! boot, and in this module's own tests) instead of leaving the observer
22//! keyed on names the document no longer carries.
23
24use std::path::Path;
25use std::sync::OnceLock;
26
27use aion_awl::TypeBody;
28use aion_package::{
29 ActionBodyContract, ContentHash, ExtractionLimits, Package, PackageError, WorkerContract,
30};
31
32/// The embedded update-check document, compiled into the binary.
33pub const EMBEDDED_UPDATE_CHECK_DOCUMENT: &str =
34 include_str!("../../update-check-embed/update-check.awl");
35
36/// The document's own filename, recorded in the assembled archive's `awl/`
37/// provenance tree.
38pub const EMBEDDED_UPDATE_CHECK_FILENAME: &str = "update-check.awl";
39
40/// The workflow type an operator starts. Also the compiled entry module, which
41/// [`EmbeddedUpdateCheck::load`] verifies.
42///
43/// # The console DUPLICATES this name, and nothing pins the two together
44///
45/// `apps/aion-ops-console/src/features/update/lib/contract.ts` carries the
46/// same literal for its "check now" start, pinned on its side by
47/// `contract.test.ts`. No build step connects that literal to this constant —
48/// TypeScript cannot see Rust. **Renaming this workflow type therefore
49/// requires renaming it there in the same change**, or every console's check
50/// button starts a workflow type that no longer exists.
51pub const UPDATE_CHECK_WORKFLOW_TYPE: &str = "update_check";
52
53/// The task queue the check's one action is declared on. No worker serves it;
54/// the declared body executes at the server.
55pub const UPDATE_CHECK_QUEUE: &str = "update_check";
56
57/// The one action: fetch the sparse-index line file for `aion-cli`.
58pub const FETCH_ACTION: &str = "fetch_crate_index";
59
60/// The declared command, verbatim as the document authors it. The observer
61/// records a check's result ONLY when the dispatching run's resolved body is
62/// exactly this command — see [`super::observer`] for why. The crate the URL
63/// ends in is the crate the index parser demands of every line
64/// (`super::index::INDEX_CRATE_NAME`); a document test pins the two together.
65pub const FETCH_COMMAND: &str = "curl -fsS https://index.crates.io/ai/on/aion-cli";
66
67/// A refusal to produce the embedded update-check package, naming the stage
68/// that refused.
69#[derive(Debug, thiserror::Error)]
70pub enum EmbeddedUpdateCheckError {
71 /// The embedded document does not parse.
72 #[error("the embedded update-check document does not parse: {message}")]
73 Parse {
74 /// The parser's diagnostic, verbatim.
75 message: String,
76 },
77
78 /// The embedded document carries a `schema(…)` import, which the binary —
79 /// embedding one file and no directory — has nothing to resolve against.
80 #[error(
81 "the embedded update-check document imports schema `{path}`, but the binary embeds the \
82 document alone and has no directory to resolve imports against; declare the type \
83 inline in the document"
84 )]
85 SchemaImport {
86 /// The import path, verbatim from the document.
87 path: String,
88 },
89
90 /// The embedded document does not compile.
91 #[error("the embedded update-check document does not compile: {message}")]
92 Compile {
93 /// The compiler's diagnostic, verbatim.
94 message: String,
95 },
96
97 /// The compiled document could not be assembled into an archive.
98 #[error("the embedded update-check document could not be packaged: {message}")]
99 Assemble {
100 /// The assembler's diagnostic, verbatim.
101 message: String,
102 },
103
104 /// The assembled archive did not load back as a validated package.
105 #[error("the embedded update-check package did not validate: {source}")]
106 Package {
107 /// The package validation failure.
108 #[from]
109 source: PackageError,
110 },
111
112 /// The compiled entry module is not [`UPDATE_CHECK_WORKFLOW_TYPE`].
113 #[error(
114 "the embedded update-check document compiles to workflow type `{found}`, but the server \
115 half in crates/aion-server/src/update_check/document.rs names \
116 `{UPDATE_CHECK_WORKFLOW_TYPE}`, so document and constants have diverged"
117 )]
118 WrongWorkflowType {
119 /// The entry module the package actually carries.
120 found: String,
121 },
122
123 /// The compiled package declares no contract at all.
124 #[error("the embedded update-check package carries no readable contract: {message}")]
125 MissingContract {
126 /// Why the contract could not be read.
127 message: String,
128 },
129
130 /// The contract does not declare [`FETCH_ACTION`] on
131 /// [`UPDATE_CHECK_QUEUE`].
132 #[error(
133 "the embedded update-check contract declares no `{FETCH_ACTION}` action on queue \
134 `{UPDATE_CHECK_QUEUE}`; the server half names both, so document and constants have \
135 diverged"
136 )]
137 MissingAction,
138
139 /// The action's declared body is absent or is not [`FETCH_COMMAND`].
140 #[error(
141 "the embedded update-check action `{FETCH_ACTION}` declares body {found:?}, but the \
142 server half records results only from `{FETCH_COMMAND}`, so document and constants \
143 have diverged"
144 )]
145 WrongBody {
146 /// The body the document actually declares, or `None` when it
147 /// declares no body at all.
148 found: Option<String>,
149 },
150}
151
152/// The embedded update check: the document's bytes and the package compiled
153/// from them.
154///
155/// Construction is the verification: holding one of these is proof that the
156/// embedded document compiled, packaged, and carries the exact queue, action,
157/// and command the server half keys on.
158#[derive(Debug, Clone)]
159pub struct EmbeddedUpdateCheck {
160 source: String,
161 package: Package,
162 workflow_type: String,
163}
164
165impl EmbeddedUpdateCheck {
166 /// Compiles, packages, and verifies the embedded document.
167 ///
168 /// # Errors
169 ///
170 /// Returns [`EmbeddedUpdateCheckError`] naming the stage that refused.
171 pub fn load() -> Result<Self, EmbeddedUpdateCheckError> {
172 Self::from_source(EMBEDDED_UPDATE_CHECK_DOCUMENT)
173 }
174
175 /// The whole preparation, over an arbitrary document.
176 ///
177 /// [`Self::load`] is this applied to the embedded bytes; it is separate so
178 /// the verification can be exercised against documents that deliberately
179 /// omit a surface — a check nothing ever fails is a check nobody has
180 /// measured. Takes any `&str` (the source is retained owned) so test
181 /// documents need no `'static` lifetime and no leak.
182 ///
183 /// # Errors
184 ///
185 /// As [`Self::load`].
186 pub fn from_source(source: &str) -> Result<Self, EmbeddedUpdateCheckError> {
187 let document =
188 aion_awl::parse(source).map_err(|error| EmbeddedUpdateCheckError::Parse {
189 message: error.message,
190 })?;
191 for declaration in &document.types {
192 if let TypeBody::SchemaImport { path, .. } = &declaration.body {
193 return Err(EmbeddedUpdateCheckError::SchemaImport { path: path.clone() });
194 }
195 }
196
197 // Never read (the load refuses schema imports above); a path that does
198 // not exist so a future import fails loudly here rather than resolving
199 // against whatever directory the server runs in. Same doctrine as the
200 // assistant embed.
201 let root = Path::new("<embedded-update-check-has-no-schema-directory>");
202 let prepared = aion_awl_package::compile_and_assemble_awl(
203 source,
204 root,
205 EMBEDDED_UPDATE_CHECK_FILENAME,
206 )
207 .map_err(|error| match error {
208 aion_awl_package::PrepareAwlError::Compile(compile) => {
209 EmbeddedUpdateCheckError::Compile {
210 message: compile.to_string(),
211 }
212 }
213 other => EmbeddedUpdateCheckError::Assemble {
214 message: other.to_string(),
215 },
216 })?;
217
218 // Trusted, compile-time content assembled by this process moments ago —
219 // not network input — so extraction carries no inflate ceiling.
220 let package = Package::load_from_bytes(&prepared.archive, ExtractionLimits::unbounded())?;
221 let workflow_type = package.manifest().entry_module.clone();
222 if workflow_type != UPDATE_CHECK_WORKFLOW_TYPE {
223 return Err(EmbeddedUpdateCheckError::WrongWorkflowType {
224 found: workflow_type,
225 });
226 }
227
228 let contract =
229 package
230 .contract()
231 .map_err(|error| EmbeddedUpdateCheckError::MissingContract {
232 message: error.to_string(),
233 })?;
234 let declared_body = contract
235 .workers
236 .iter()
237 .filter(|worker: &&WorkerContract| worker.task_queue == UPDATE_CHECK_QUEUE)
238 .flat_map(|worker| worker.actions.iter())
239 .find(|action| action.name == FETCH_ACTION)
240 .ok_or(EmbeddedUpdateCheckError::MissingAction)?
241 .body
242 .clone();
243 match declared_body {
244 Some(ActionBodyContract::Run { ref command }) if command == FETCH_COMMAND => {}
245 Some(ActionBodyContract::Run { command }) => {
246 return Err(EmbeddedUpdateCheckError::WrongBody {
247 found: Some(command),
248 });
249 }
250 // The embedded document declares a `run` body and only a `run`
251 // body. A declared-command body here would be a different program
252 // with a different argument list, so it is refused by name rather
253 // than accepted as "close enough".
254 Some(ActionBodyContract::Command { command, .. }) => {
255 return Err(EmbeddedUpdateCheckError::WrongBody {
256 found: Some(format!("runs command {}", command.name)),
257 });
258 }
259 None => return Err(EmbeddedUpdateCheckError::WrongBody { found: None }),
260 }
261
262 Ok(Self {
263 source: source.to_owned(),
264 package,
265 workflow_type,
266 })
267 }
268
269 /// The validated package the engine loads.
270 #[must_use]
271 pub const fn package(&self) -> &Package {
272 &self.package
273 }
274
275 /// The workflow type an operator starts — [`UPDATE_CHECK_WORKFLOW_TYPE`],
276 /// by construction.
277 #[must_use]
278 pub fn workflow_type(&self) -> &str {
279 &self.workflow_type
280 }
281
282 /// The package's content hash — this document's version identity.
283 #[must_use]
284 pub const fn content_hash(&self) -> &ContentHash {
285 self.package.content_hash()
286 }
287
288 /// The document source, verbatim.
289 #[must_use]
290 pub fn source(&self) -> &str {
291 &self.source
292 }
293}
294
295/// The process-wide embedded update check, compiled once on first use.
296///
297/// # Errors
298///
299/// Returns the [`EmbeddedUpdateCheckError`] from the single load attempt.
300pub fn embedded_update_check()
301-> Result<&'static EmbeddedUpdateCheck, &'static EmbeddedUpdateCheckError> {
302 static EMBEDDED: OnceLock<Result<EmbeddedUpdateCheck, EmbeddedUpdateCheckError>> =
303 OnceLock::new();
304 EMBEDDED.get_or_init(EmbeddedUpdateCheck::load).as_ref()
305}
306
307#[cfg(test)]
308#[path = "document_tests.rs"]
309mod document_tests;