use aion_integration_acp::catalogue;
use crate::error::ServerError;
use super::super::{
config_error,
defaults::{
ASSISTANT_ACCOUNT_ENV_CREDENTIAL_SHAPED, ASSISTANT_ACCOUNT_ENV_NAME_INVALID,
ASSISTANT_ACCOUNT_NAME_DUPLICATE, ASSISTANT_ACCOUNT_NAME_REQUIRED,
ASSISTANT_HARNESS_NAME_DUPLICATE, ASSISTANT_HARNESS_NAME_REQUIRED,
ASSISTANT_HARNESS_NAME_UNKNOWN, CREDENTIAL_SHAPED_ENV_NAME_FRAGMENTS,
},
};
use super::{
AssistantAccountConfig, AssistantConfig, AssistantHarnessConfig, ResolvedAssistantAccount,
ResolvedAssistantConfig, ResolvedAssistantHarness,
};
impl AssistantConfig {
pub(in crate::config) fn validate(&self) -> Result<(), ServerError> {
self.resolve_checked().map(drop)
}
#[must_use]
pub fn resolved(&self) -> ResolvedAssistantConfig {
self.resolve_checked().unwrap_or_default()
}
fn resolve_checked(&self) -> Result<ResolvedAssistantConfig, ServerError> {
let mut harnesses: Vec<ResolvedAssistantHarness> = Vec::with_capacity(self.harnesses.len());
for harness in &self.harnesses {
let resolved = resolve_harness(harness)?;
if harnesses.iter().any(|seen| seen.name == resolved.name) {
return config_error(format!(
"{ASSISTANT_HARNESS_NAME_DUPLICATE}: two [[assistant.harness]] entries are \
both named `{}`; accounts are looked up by harness name, so a repeated name \
makes the lookup ambiguous — put every account for one harness in a single \
entry",
resolved.name
));
}
harnesses.push(resolved);
}
Ok(ResolvedAssistantConfig { harnesses })
}
}
fn resolve_harness(
harness: &AssistantHarnessConfig,
) -> Result<ResolvedAssistantHarness, ServerError> {
let name = required_name(
harness.name.as_deref(),
ASSISTANT_HARNESS_NAME_REQUIRED,
"assistant.harness.name",
)?;
if catalogue::harness(&name).is_none() {
return config_error(format!(
"{ASSISTANT_HARNESS_NAME_UNKNOWN}: `{name}` is not a harness this build ships, so \
nothing could ever be started on it. This build ships: {}. The launch command is the \
catalogue's own — there is no command, path or argument to declare here, only the \
accounts an operator may pick from.",
catalogue::ids()
));
}
let mut accounts: Vec<ResolvedAssistantAccount> = Vec::with_capacity(harness.accounts.len());
for account in &harness.accounts {
let resolved = resolve_account(account, &name)?;
if accounts.iter().any(|seen| seen.name == resolved.name) {
return config_error(format!(
"{ASSISTANT_ACCOUNT_NAME_DUPLICATE}: harness `{name}` declares two accounts both \
named `{}`; an account is selected by name, so a repeated name makes the \
selection ambiguous",
resolved.name
));
}
accounts.push(resolved);
}
Ok(ResolvedAssistantHarness { name, accounts })
}
fn required_name(name: Option<&str>, prefix: &str, key: &str) -> Result<String, ServerError> {
match name {
Some(name) if !name.is_empty() => Ok(name.to_owned()),
Some(_) => config_error(format!("{prefix}: {key} is empty")),
None => config_error(format!("{prefix}: {key} is required and has no default")),
}
}
fn resolve_account(
account: &AssistantAccountConfig,
harness: &str,
) -> Result<ResolvedAssistantAccount, ServerError> {
let name = required_name(
account.name.as_deref(),
ASSISTANT_ACCOUNT_NAME_REQUIRED,
&format!("assistant.harness.account.name in harness `{harness}`"),
)?;
for (child, source) in &account.env {
validate_variable_name(child, &name, harness, "the name given to the agent")?;
validate_variable_name(
source,
&name,
harness,
"the name it is read from in the server's own environment",
)?;
let lowered = child.to_ascii_lowercase();
if let Some(fragment) = CREDENTIAL_SHAPED_ENV_NAME_FRAGMENTS
.iter()
.find(|fragment| lowered.contains(*fragment))
{
return config_error(format!(
"{ASSISTANT_ACCOUNT_ENV_CREDENTIAL_SHAPED}: account `{name}` in harness \
`{harness}` declares `{child}`, whose name contains `{fragment}`. A credential is \
the harness's OWN login state on disk and is never ours to carry: log in out of \
band on the server host under this account's config directory. This env table \
says WHICH on-disk login state to use, nothing more"
));
}
}
Ok(ResolvedAssistantAccount {
name,
env: account
.env
.iter()
.map(|(child, source)| (child.clone(), source.clone()))
.collect(),
})
}
fn validate_variable_name(
variable: &str,
account: &str,
harness: &str,
side: &str,
) -> Result<(), ServerError> {
let usable = !variable.is_empty()
&& !variable.starts_with(|first: char| first.is_ascii_digit())
&& variable
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_');
if usable {
return Ok(());
}
config_error(format!(
"{ASSISTANT_ACCOUNT_ENV_NAME_INVALID}: account `{account}` in harness `{harness}` declares \
`{variable}` as {side}, which is not an environment variable name (letters, digits and \
underscores, not starting with a digit). Both sides of this table are NAMES — the value \
is taken from the server's own environment at spawn, so a path or a secret written here \
would be a value in a file that gets committed"
))
}