aion_awl/workspace_root.rs
1//! The `{workspace_root}` placeholder, and the two forms a harness path may take.
2//!
3//! # Why a document may not simply write the path
4//!
5//! A `harness` section's path-valued settings — an ACP agent's `command`, either kind's
6//! `cwd`, a Norn `binary` — are absolute by rule, because a worker is started from
7//! whatever directory a supervisor or a shell happens to be in and a relative path names
8//! a different place on every launch.
9//!
10//! That rule is right for a document an operator writes for THEIR box, and impossible
11//! for a document that SHIPS. A document compiled into the server binary must be correct
12//! on every machine that runs it, and there is no absolute literal that is right on more
13//! than one. Held to absolute-only, such a document cannot be written at all.
14//!
15//! So a second accepted form exists, and exactly one: a value that LEADS with the
16//! literal [`WORKSPACE_ROOT_PLACEHOLDER`]. The document states the shape of the path and
17//! the box supplies its root, which is the same division of labour the server already
18//! uses for declared action bodies (`aion_server::worker::WorkspaceRoot`).
19//!
20//! # Where each end of that division is enforced
21//!
22//! This module is the ACCEPT-SET only: it says which of the two forms a value is, and
23//! refuses everything else with a reason. It deliberately cannot expand anything —
24//! resolving the root is a property of the box, which the language layer has no business
25//! knowing.
26//!
27//! The composition root that launches the harness expands the placeholder against the
28//! box's own root and THEN demands the result be absolute. Both ends therefore hold:
29//! nothing about the absolute requirement is softened, and a placeholder that expands to
30//! a non-absolute path — or one on a box whose root cannot be resolved at all — is
31//! refused there, loudly, before anything is spawned.
32//!
33//! # Why the placeholder's spelling lives HERE
34//!
35//! It is a token of the language documents are written in, and the server's declared-body
36//! expansion, the checker, and the compiler's backstop must all agree on it to the byte.
37//! One definition in the crate every one of them already depends on is that agreement;
38//! a second copy anywhere is the drift.
39
40/// The literal a harness path setting carries where the box's workspace root belongs.
41///
42/// Braces are literal text to the AWL lexer and to the worker SDK's command template
43/// (`$name`/`${name}` are its only parameter forms), so a value carrying this checks
44/// clean and — if it ever reached a launch unexpanded — would fail loudly on a
45/// nonexistent `{workspace_root}` path rather than silently launching somewhere else.
46pub const WORKSPACE_ROOT_PLACEHOLDER: &str = "{workspace_root}";
47
48/// Which of the two accepted forms a harness path setting is written in.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum HarnessPathForm {
51 /// An absolute path, usable exactly as written.
52 Absolute,
53 /// A path leading with [`WORKSPACE_ROOT_PLACEHOLDER`], absolute only once the
54 /// composition root has expanded it against the box's own workspace root.
55 WorkspaceRooted,
56}
57
58/// Why a harness path setting is neither accepted form.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum HarnessPathRefusal {
61 /// The value is empty or nothing but whitespace.
62 Empty,
63 /// The value is a relative path and carries no placeholder to make it otherwise.
64 Relative,
65 /// The value carries the placeholder, but not at its start — so however the box
66 /// resolves, the expansion cannot produce an absolute path.
67 PlaceholderNotLeading,
68}
69
70impl HarnessPathRefusal {
71 /// The refusal's reason, a self-contained sentence opening with "it".
72 ///
73 /// One phrasing per refusal, read by both the checker's operator-facing message and
74 /// the compiler's backstop, so the two cannot come to describe the same fault
75 /// differently. It opens with "it" because both slots introduce the offending setting
76 /// first and the reason second.
77 #[must_use]
78 pub const fn reason(self) -> &'static str {
79 match self {
80 Self::Empty => {
81 "it is empty; a path setting names an absolute path, or leads with \
82 `{workspace_root}` for the launching box to expand"
83 }
84 Self::Relative => {
85 "it must be an ABSOLUTE path. A worker is started from whatever directory a \
86 supervisor or a shell happens to be in, so a relative path names a different \
87 place on every launch. A document that must be correct on more than one box \
88 leads the value with `{workspace_root}` instead, which the launching worker \
89 expands against that box's own workspace root."
90 }
91 Self::PlaceholderNotLeading => {
92 "it carries `{workspace_root}` somewhere other than its start, so expanding it \
93 cannot produce an absolute path. The placeholder stands at the beginning of \
94 the value or not at all."
95 }
96 }
97 }
98}
99
100/// Which accepted form `value` is written in, or why it is neither.
101///
102/// The accept-set is EXACTLY two forms and is stated only here: the checker refuses a
103/// document by this rule, and the compiler's backstop refuses a value that reached it
104/// by any other route by the same one.
105///
106/// # Errors
107///
108/// Returns the [`HarnessPathRefusal`] naming which of the three ways the value failed to
109/// be either accepted form.
110pub fn harness_path_form(value: &str) -> Result<HarnessPathForm, HarnessPathRefusal> {
111 if value.trim().is_empty() {
112 return Err(HarnessPathRefusal::Empty);
113 }
114 if value.starts_with(WORKSPACE_ROOT_PLACEHOLDER) {
115 return Ok(HarnessPathForm::WorkspaceRooted);
116 }
117 if value.contains(WORKSPACE_ROOT_PLACEHOLDER) {
118 return Err(HarnessPathRefusal::PlaceholderNotLeading);
119 }
120 if std::path::Path::new(value).is_absolute() {
121 return Ok(HarnessPathForm::Absolute);
122 }
123 Err(HarnessPathRefusal::Relative)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::{
129 HarnessPathForm, HarnessPathRefusal, WORKSPACE_ROOT_PLACEHOLDER, harness_path_form,
130 };
131
132 #[test]
133 fn an_absolute_path_is_the_first_accepted_form() {
134 assert_eq!(
135 harness_path_form("/srv/agents/workspace"),
136 Ok(HarnessPathForm::Absolute)
137 );
138 }
139
140 #[test]
141 fn a_leading_placeholder_is_the_second_accepted_form() {
142 assert_eq!(
143 harness_path_form(WORKSPACE_ROOT_PLACEHOLDER),
144 Ok(HarnessPathForm::WorkspaceRooted)
145 );
146 assert_eq!(
147 harness_path_form("{workspace_root}/assistant"),
148 Ok(HarnessPathForm::WorkspaceRooted)
149 );
150 }
151
152 #[test]
153 fn a_relative_path_is_refused_as_it_always_was() {
154 assert_eq!(
155 harness_path_form("workspace"),
156 Err(HarnessPathRefusal::Relative)
157 );
158 assert_eq!(
159 harness_path_form("./workspace"),
160 Err(HarnessPathRefusal::Relative)
161 );
162 assert_eq!(
163 harness_path_form("../workspace"),
164 Err(HarnessPathRefusal::Relative)
165 );
166 }
167
168 #[test]
169 fn the_new_form_did_not_widen_the_old_refusal() {
170 // The placeholder buys a document ONE shape: the root at the front. A value that
171 // merely mentions it is still refused, because no root the box resolves can make
172 // `agents/{workspace_root}` absolute — accepting it here would hand the launch a
173 // value the composition root must refuse, which is the checker-green/launch-red
174 // split the checker exists to prevent.
175 assert_eq!(
176 harness_path_form("agents/{workspace_root}"),
177 Err(HarnessPathRefusal::PlaceholderNotLeading)
178 );
179 assert_eq!(
180 harness_path_form("/srv/{workspace_root}/agents"),
181 Err(HarnessPathRefusal::PlaceholderNotLeading),
182 "an absolute path carrying the placeholder is still refused: the expansion \
183 would splice a second root into the middle of it"
184 );
185 }
186
187 #[test]
188 fn an_empty_value_is_refused_before_either_form_is_considered() {
189 assert_eq!(harness_path_form(""), Err(HarnessPathRefusal::Empty));
190 assert_eq!(harness_path_form(" "), Err(HarnessPathRefusal::Empty));
191 }
192}