aion_server/config/
assistant_resolve.rs1use aion_integration_acp::catalogue;
25
26use crate::error::ServerError;
27
28use super::super::{
29 config_error,
30 defaults::{
31 ASSISTANT_ACCOUNT_ENV_CREDENTIAL_SHAPED, ASSISTANT_ACCOUNT_ENV_NAME_INVALID,
32 ASSISTANT_ACCOUNT_NAME_DUPLICATE, ASSISTANT_ACCOUNT_NAME_REQUIRED,
33 ASSISTANT_HARNESS_NAME_DUPLICATE, ASSISTANT_HARNESS_NAME_REQUIRED,
34 ASSISTANT_HARNESS_NAME_UNKNOWN, CREDENTIAL_SHAPED_ENV_NAME_FRAGMENTS,
35 },
36};
37use super::{
38 AssistantAccountConfig, AssistantConfig, AssistantHarnessConfig, ResolvedAssistantAccount,
39 ResolvedAssistantConfig, ResolvedAssistantHarness,
40};
41
42impl AssistantConfig {
43 pub(in crate::config) fn validate(&self) -> Result<(), ServerError> {
53 self.resolve_checked().map(drop)
54 }
55
56 #[must_use]
66 pub fn resolved(&self) -> ResolvedAssistantConfig {
67 self.resolve_checked().unwrap_or_default()
68 }
69
70 fn resolve_checked(&self) -> Result<ResolvedAssistantConfig, ServerError> {
72 let mut harnesses: Vec<ResolvedAssistantHarness> = Vec::with_capacity(self.harnesses.len());
73 for harness in &self.harnesses {
74 let resolved = resolve_harness(harness)?;
75 if harnesses.iter().any(|seen| seen.name == resolved.name) {
76 return config_error(format!(
77 "{ASSISTANT_HARNESS_NAME_DUPLICATE}: two [[assistant.harness]] entries are \
78 both named `{}`; accounts are looked up by harness name, so a repeated name \
79 makes the lookup ambiguous — put every account for one harness in a single \
80 entry",
81 resolved.name
82 ));
83 }
84 harnesses.push(resolved);
85 }
86 Ok(ResolvedAssistantConfig { harnesses })
87 }
88}
89
90fn resolve_harness(
92 harness: &AssistantHarnessConfig,
93) -> Result<ResolvedAssistantHarness, ServerError> {
94 let name = required_name(
95 harness.name.as_deref(),
96 ASSISTANT_HARNESS_NAME_REQUIRED,
97 "assistant.harness.name",
98 )?;
99 if catalogue::harness(&name).is_none() {
100 return config_error(format!(
101 "{ASSISTANT_HARNESS_NAME_UNKNOWN}: `{name}` is not a harness this build ships, so \
102 nothing could ever be started on it. This build ships: {}. The launch command is the \
103 catalogue's own — there is no command, path or argument to declare here, only the \
104 accounts an operator may pick from.",
105 catalogue::ids()
106 ));
107 }
108 let mut accounts: Vec<ResolvedAssistantAccount> = Vec::with_capacity(harness.accounts.len());
109 for account in &harness.accounts {
110 let resolved = resolve_account(account, &name)?;
111 if accounts.iter().any(|seen| seen.name == resolved.name) {
112 return config_error(format!(
113 "{ASSISTANT_ACCOUNT_NAME_DUPLICATE}: harness `{name}` declares two accounts both \
114 named `{}`; an account is selected by name, so a repeated name makes the \
115 selection ambiguous",
116 resolved.name
117 ));
118 }
119 accounts.push(resolved);
120 }
121 Ok(ResolvedAssistantHarness { name, accounts })
122}
123
124fn required_name(name: Option<&str>, prefix: &str, key: &str) -> Result<String, ServerError> {
126 match name {
127 Some(name) if !name.is_empty() => Ok(name.to_owned()),
128 Some(_) => config_error(format!("{prefix}: {key} is empty")),
129 None => config_error(format!("{prefix}: {key} is required and has no default")),
130 }
131}
132
133fn resolve_account(
135 account: &AssistantAccountConfig,
136 harness: &str,
137) -> Result<ResolvedAssistantAccount, ServerError> {
138 let name = required_name(
139 account.name.as_deref(),
140 ASSISTANT_ACCOUNT_NAME_REQUIRED,
141 &format!("assistant.harness.account.name in harness `{harness}`"),
142 )?;
143 for (child, source) in &account.env {
144 validate_variable_name(child, &name, harness, "the name given to the agent")?;
150 validate_variable_name(
151 source,
152 &name,
153 harness,
154 "the name it is read from in the server's own environment",
155 )?;
156 let lowered = child.to_ascii_lowercase();
157 if let Some(fragment) = CREDENTIAL_SHAPED_ENV_NAME_FRAGMENTS
158 .iter()
159 .find(|fragment| lowered.contains(*fragment))
160 {
161 return config_error(format!(
162 "{ASSISTANT_ACCOUNT_ENV_CREDENTIAL_SHAPED}: account `{name}` in harness \
163 `{harness}` declares `{child}`, whose name contains `{fragment}`. A credential is \
164 the harness's OWN login state on disk and is never ours to carry: log in out of \
165 band on the server host under this account's config directory. This env table \
166 says WHICH on-disk login state to use, nothing more"
167 ));
168 }
169 }
170 Ok(ResolvedAssistantAccount {
171 name,
172 env: account
173 .env
174 .iter()
175 .map(|(child, source)| (child.clone(), source.clone()))
176 .collect(),
177 })
178}
179
180fn validate_variable_name(
187 variable: &str,
188 account: &str,
189 harness: &str,
190 side: &str,
191) -> Result<(), ServerError> {
192 let usable = !variable.is_empty()
193 && !variable.starts_with(|first: char| first.is_ascii_digit())
194 && variable
195 .chars()
196 .all(|character| character.is_ascii_alphanumeric() || character == '_');
197 if usable {
198 return Ok(());
199 }
200 config_error(format!(
201 "{ASSISTANT_ACCOUNT_ENV_NAME_INVALID}: account `{account}` in harness `{harness}` declares \
202 `{variable}` as {side}, which is not an environment variable name (letters, digits and \
203 underscores, not starting with a digit). Both sides of this table are NAMES — the value \
204 is taken from the server's own environment at spawn, so a path or a secret written here \
205 would be a value in a file that gets committed"
206 ))
207}