aion-server 0.30.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! The `[assistant]` section: optional, and about ACCOUNTS only.
//!
//! # The section is optional, and a stock server serves the assistant
//!
//! RULED 2026-08-29 (Tom, "it needs to work out of the box"): a first install
//! runs the assistant with no configuration, no environment variables and no
//! second process. The harness catalogue ships as DATA in
//! [`aion_integration_acp::catalogue`] — id, launch line, install hint — so
//! there is no command, no path, no working directory and no timeout for an
//! operator to type, and nothing to get wrong before the first message.
//!
//! What is left in this section is the one thing the product genuinely cannot
//! know: which named LOGIN ACCOUNTS a deployment wants offered on a harness, and
//! which environment variables select each one. Everything else that used to
//! live here — `default_harness`, `spawn_timeout_ms`, `turn_timeout_ms`,
//! `event_buffer`, and every per-harness `command` / `args` / `cwd` /
//! `env_pass` / `permission` / `exit_grace_ms` / `tool_confinement` — is
//! RETIRED, not defaulted: see the round-2 amendment. A file that still carries
//! one is refused at load naming the key, because a knob that is silently
//! ignored is worse than one that is gone.
//!
//! # Accounts carry NAMES on both sides
//!
//! An account's `env` maps the variable name the CHILD gets to the variable name
//! it is READ FROM in the server's own environment:
//!
//! ```toml
//! [[assistant.harness]]
//! name = "claude-code"
//!
//! [[assistant.harness.account]]
//! name = "work"
//! env = { CLAUDE_CONFIG_DIR = "AION_CLAUDE_WORK_DIR" }
//! ```
//!
//! Both sides are names. No value is ever written into this file: a value in a
//! document gets committed, diffed and deployed, and the value here would be
//! pointing at somebody's login state. The value comes from the server's own
//! environment at spawn, and a declared source name the server does not carry is
//! a TYPED ABSENCE at spawn rather than an empty string handed to an agent.
//!
//! A credential-shaped variable NAME is still refused at load. A credential is
//! the harness's own login state on disk, never ours to carry: the operator logs
//! the harness in on the server host, under that account's config directory, and
//! this server neither prompts for nor stores one.

use std::collections::BTreeMap;

use serde::Deserialize;

/// The validating resolution: every `[assistant]` refusal, and the pass that
/// turns the wire shapes below into the resolved ones.
#[path = "assistant_resolve.rs"]
mod resolve;

/// The `[assistant]` section — entirely optional.
///
/// [`Default`] is the empty section, which is what a server with no
/// `[assistant]` at all gets, and it is a COMPLETE configuration: every
/// catalogue harness is offered, none of them declares an account, and sessions
/// are served. Absence is not darkness here.
///
/// `deny_unknown_fields` is what refuses a retired knob by name: a file carrying
/// `turn_timeout_ms` is told that no such key exists rather than being loaded
/// with a value nothing reads.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct AssistantConfig {
    /// Per-harness account declarations, from `[[assistant.harness]]`. Empty is
    /// a complete answer: every harness the catalogue ships is still offered,
    /// with no accounts on it.
    #[serde(rename = "harness")]
    pub harnesses: Vec<AssistantHarnessConfig>,
}

/// The accounts declared on ONE catalogue harness, from
/// `[[assistant.harness]]`.
///
/// It declares no launch of its own: `name` selects a harness the build already
/// ships, and everything else about how that harness is started is the
/// catalogue's.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct AssistantHarnessConfig {
    /// The catalogue id these accounts belong to (`claude-code`, `codex`, …).
    /// REQUIRED, non-empty, and refused when the build ships no such harness.
    pub name: Option<String>,
    /// Named login accounts for it, from `[[assistant.harness.account]]`.
    /// Empty is a complete answer.
    #[serde(rename = "account")]
    pub accounts: Vec<AssistantAccountConfig>,
}

/// One named login account, from `[[assistant.harness.account]]`.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct AssistantAccountConfig {
    /// The account's name, unique within its harness. REQUIRED, non-empty.
    pub name: Option<String>,
    /// `CHILD_VARIABLE = "SERVER_VARIABLE"`: which variable the spawned agent
    /// gets, and which variable of the server's own environment its value is
    /// read from. NAMES on both sides — never a value — and a
    /// credential-shaped child name is refused at load. Empty is a complete
    /// answer.
    pub env: BTreeMap<String, String>,
}

/// The `[assistant]` section, validated.
///
/// [`Default`] is the stock server: no accounts declared anywhere. It is not a
/// dark form and there is no `enabled` flag — whether a session can be opened is
/// a question about the STORE and about which catalogue harness the operator
/// picked, both answered where those facts are, not by a config field.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ResolvedAssistantConfig {
    /// The harnesses that declare accounts, in declaration order. A catalogue
    /// harness absent from this list is still offered; it simply has none.
    pub harnesses: Vec<ResolvedAssistantHarness>,
}

impl ResolvedAssistantConfig {
    /// The accounts declared for catalogue harness `name`, or [`None`] when it
    /// declares none.
    #[must_use]
    pub fn harness(&self, name: &str) -> Option<&ResolvedAssistantHarness> {
        self.harnesses.iter().find(|harness| harness.name == name)
    }

    /// The account names declared for `harness`, in declaration order — what
    /// the descriptor publishes. Empty when none is declared, which is the
    /// stock answer.
    #[must_use]
    pub fn account_names(&self, harness: &str) -> Vec<String> {
        self.harness(harness).map_or_else(Vec::new, |harness| {
            harness
                .accounts
                .iter()
                .map(|account| account.name.clone())
                .collect()
        })
    }

    /// The account `account_name` declared on `harness`, or [`None`].
    #[must_use]
    pub fn account(&self, harness: &str, account_name: &str) -> Option<&ResolvedAssistantAccount> {
        self.harness(harness)?.account(account_name)
    }
}

/// One catalogue harness's declared accounts, validated.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedAssistantHarness {
    /// The catalogue id.
    pub name: String,
    /// Its named login accounts, in declaration order.
    pub accounts: Vec<ResolvedAssistantAccount>,
}

impl ResolvedAssistantHarness {
    /// The account declared under `name` on this harness, or [`None`].
    #[must_use]
    pub fn account(&self, name: &str) -> Option<&ResolvedAssistantAccount> {
        self.accounts.iter().find(|account| account.name == name)
    }
}

/// One named login account, validated.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedAssistantAccount {
    /// The account's name, unique within its harness.
    pub name: String,
    /// `(child variable, server variable it is read from)`, in sorted child-name
    /// order. NAMES on both sides: the values are resolved from the server's
    /// environment at spawn, and a source the server does not carry is a typed
    /// absence there rather than an empty string here.
    pub env: Vec<(String, String)>,
}

impl ResolvedAssistantAccount {
    /// The server-environment variable names this account reads, in the order
    /// its pairs are declared.
    ///
    /// What the spawn's environment declaration is extended with, so the
    /// existing `EnvironmentDeclaration` discipline — names resolved against the
    /// process, absences reported — covers an account's variables exactly as it
    /// covers the harness's own.
    #[must_use]
    pub fn source_names(&self) -> Vec<String> {
        self.env
            .iter()
            .map(|(_child, source)| source.clone())
            .collect()
    }
}

#[cfg(test)]
#[path = "assistant_tests.rs"]
mod tests;