aion_server/worker/auto_provision/queues.rs
1//! Which queues of a deployed document declare a built-in agent worker.
2//!
3//! # Why this is read from the SOURCE and not from the contract
4//!
5//! A `.aion` package's identity-bound `PackageContract` carries the task queue
6//! and, per action, whether it is an agent seam — but it carries nothing about
7//! the `harness` section: not the kind, not the concurrency, not the agent's
8//! command or environment. The section is deliberately not flow meaning (the
9//! MIR ratchet pins a document WITH one to the same lowering as the same
10//! document WITHOUT one), so it never reaches the contract.
11//!
12//! What DOES reach the server is the archive's AWL provenance: the authored
13//! document verbatim, plus the schema files it imports. That is the only place
14//! the harness section exists on this side of the wire, so that is where this
15//! reads it.
16//!
17//! # Auto-provision keys on the SECTION, not on deploys generally
18//!
19//! A document with no `harness` section mints nothing. That is not a fallback —
20//! a queue with no section is the shape of a queue served by a worker SDK
21//! build, by `aion worker shell`, or by declared `run` bodies, and standing a
22//! built-in agent worker up on it would be the server inventing a launch
23//! nobody declared.
24
25use aion_awl::{CompiledHarness, WorkerDecl};
26
27/// One queue of a deployed document that declares its own agent launch.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct HarnessQueue {
30 /// The task queue — the `worker` block's own name.
31 pub task_queue: String,
32 /// The compiled launch, proving the section is complete for its kind.
33 ///
34 /// Held rather than discarded because compiling it is the ONLY thing that
35 /// proves an `aion worker agent` on this document would start: a section
36 /// that refuses at the worker would otherwise be minted here and discovered
37 /// as a crash loop.
38 pub harness: CompiledHarness,
39}
40
41/// Why a deployed document's harness queues could not be read.
42#[derive(Debug, thiserror::Error)]
43pub enum HarnessQueueError {
44 /// The archived document did not parse.
45 ///
46 /// The package still loaded — its identity is the compiled beams, and the
47 /// archived source is provenance — so this names a package whose carried
48 /// source disagrees with what it was built from.
49 #[error(
50 "the deployed package's archived AWL document could not be parsed, so this server \
51 cannot tell whether it declares a built-in agent worker: {message}"
52 )]
53 Parse {
54 /// The parser's own diagnosis.
55 message: String,
56 },
57 /// A `harness` section is present but incomplete for the kind it declares.
58 #[error(
59 "task queue `{task_queue}` declares a `harness` section that is not complete: {message}. \
60 Nothing was minted for it — fix the section, run `aion awl check`, and deploy again"
61 )]
62 Harness {
63 /// The queue whose section refused.
64 task_queue: String,
65 /// The checker's own refusal.
66 message: String,
67 },
68 /// Every action on the queue carries a declared body, so an agent worker
69 /// would be started and never dispatched anything.
70 #[error(
71 "task queue `{task_queue}` declares a `harness` section but every one of its actions \
72 ({actions}) carries a declared `run` body, which the server executes from the deployed \
73 contract. There is nothing on that queue for an agent worker to serve, so none was \
74 minted"
75 )]
76 NoServiceableAction {
77 /// The queue with nothing to serve.
78 task_queue: String,
79 /// The action names, comma-separated, that all carry bodies.
80 actions: String,
81 },
82}
83
84/// Read every queue in `source` that declares a built-in agent launch.
85///
86/// An empty result is the ordinary answer, not a failure: most documents
87/// declare no `harness` section and mint nothing.
88///
89/// # Errors
90///
91/// Returns [`HarnessQueueError::Parse`] when the archived document does not
92/// parse, and — for a queue that DOES declare a section —
93/// [`HarnessQueueError::Harness`] when the section is incomplete or
94/// [`HarnessQueueError::NoServiceableAction`] when the queue has nothing an
95/// agent could be dispatched. A queue with no section contributes neither a
96/// result nor an error.
97pub fn harness_queues(source: &str) -> Result<Vec<HarnessQueue>, HarnessQueueError> {
98 let document = aion_awl::parse(source).map_err(|error| HarnessQueueError::Parse {
99 message: error.to_string(),
100 })?;
101 let mut queues = Vec::new();
102 for worker in &document.workers {
103 if worker.harness.is_none() {
104 continue;
105 }
106 queues.push(read_queue(worker)?);
107 }
108 Ok(queues)
109}
110
111/// Compile one declaring worker block into a mintable queue, or say why not.
112fn read_queue(worker: &WorkerDecl) -> Result<HarnessQueue, HarnessQueueError> {
113 let compiled =
114 aion_awl::compile_harness(worker).map_err(|error| HarnessQueueError::Harness {
115 task_queue: worker.name.clone(),
116 message: error.to_string(),
117 })?;
118 // `compile_harness` answers `None` only for a block with NO section, which
119 // the caller already ruled out. Reporting the impossible arm as the section
120 // refusing keeps this total without a panic and without a silent skip.
121 let Some(harness) = compiled else {
122 return Err(HarnessQueueError::Harness {
123 task_queue: worker.name.clone(),
124 message: "the section vanished between the presence test and the compile".to_owned(),
125 });
126 };
127 let bodied: Vec<&str> = worker
128 .actions
129 .iter()
130 .filter(|action| action.body.is_some())
131 .map(|action| action.name.as_str())
132 .collect();
133 if bodied.len() == worker.actions.len() {
134 return Err(HarnessQueueError::NoServiceableAction {
135 task_queue: worker.name.clone(),
136 actions: if bodied.is_empty() {
137 "the queue declares none".to_owned()
138 } else {
139 bodied.join(", ")
140 },
141 });
142 }
143 Ok(HarnessQueue {
144 task_queue: worker.name.clone(),
145 harness,
146 })
147}
148
149#[cfg(test)]
150#[path = "queues_tests.rs"]
151mod tests;