aion-server 0.29.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! The typed refusals the assistant-session surface answers with.
//!
//! Every one of them names the thing that is wrong and, where there is one, the
//! remedy. None of them is a catch-all: an operator reading a refusal must be
//! able to tell "you have no grant" from "that harness is not configured" from
//! "the agent needs logging in", because the three have three different
//! answers.

use aion_core::AssistantSessionId;
use aion_store::StoreError;

/// A refusal from the assistant-session surface.
#[derive(Debug, thiserror::Error)]
pub enum AssistantSessionError {
    /// The assistant surface cannot open a session on this server at all.
    ///
    /// Reserved for a refusal the PRODUCT can name — the store this server
    /// writes sessions to is unusable — and never for "not configured": a stock
    /// server with no `[assistant]` section serves the assistant.
    #[error("this server cannot open an assistant session: {reason}")]
    NotCommissioned {
        /// Why, in words an operator can act on.
        reason: String,
    },

    /// The caller named a harness this build does not ship.
    #[error("no assistant harness named `{requested}` exists; this build ships {declared}")]
    UnknownHarness {
        /// What the caller asked for.
        requested: String,
        /// The catalogue's ids, rendered for the message.
        declared: String,
    },

    /// The chosen harness's launch program does not resolve on this server's
    /// `PATH`.
    ///
    /// The SAME shape whether it is met at selection or at spawn, because it is
    /// the same fact measured at two moments: `createSession` refuses rather
    /// than accepting a session that could only fail later, and the spawn
    /// re-measures because a machine can change between the two.
    #[error(
        "the assistant harness `{harness}` cannot be started on this server: its launch command \
         `{launch}` is not on the server's PATH. {install_hint}"
    )]
    HarnessUnavailable {
        /// The catalogue id that cannot run here.
        harness: String,
        /// The exact line this server would have run.
        launch: String,
        /// The catalogue's own sentence naming what to install.
        install_hint: String,
    },

    /// An account names a server environment variable this server does not
    /// carry.
    ///
    /// A typed absence, never an empty string: an agent handed an empty config
    /// directory looks logged out, and the operator has nothing to read that
    /// would say why.
    #[error(
        "the assistant account `{account}` on harness `{harness}` names environment variables this \
         server does not carry: {variables}. The account declares NAMES; the values come from the \
         server's own environment, so set them where the server is started."
    )]
    AccountEnvironmentAbsent {
        /// The harness the account belongs to.
        harness: String,
        /// The account that cannot be honoured.
        account: String,
        /// The absent variables, each with the name the agent would have got.
        variables: String,
    },

    /// The caller named an account the harness does not declare.
    #[error(
        "the assistant harness `{harness}` declares no account named `{requested}`; it declares \
         {declared}"
    )]
    UnknownAccount {
        /// The harness that was asked.
        harness: String,
        /// What the caller asked for.
        requested: String,
        /// The declared names, rendered for the message.
        declared: String,
    },

    /// The session id names nothing this server holds.
    #[error("assistant session {session_id} was not found")]
    NotFound {
        /// The session that was asked for.
        session_id: AssistantSessionId,
    },

    /// The session belongs to another caller.
    ///
    /// Reported as NOT FOUND on the wire, never as "forbidden": a caller must
    /// not learn that another subject's session exists. The distinction is kept
    /// here so the server's own audit line can say what really happened.
    #[error("assistant session {session_id} does not belong to subject `{subject}`")]
    NotYours {
        /// The session that was asked for.
        session_id: AssistantSessionId,
        /// The caller that asked.
        subject: String,
    },

    /// A turn was submitted while one is already open.
    #[error(
        "assistant session {session_id} already has a turn open — an agent holds one \
         conversation, so a second concurrent turn could not be attributed; cancel the open turn \
         or wait for its result"
    )]
    Busy {
        /// The session that is busy.
        session_id: AssistantSessionId,
    },

    /// A turn invoked a command the harness never advertised.
    ///
    /// Refused HERE rather than sent: an agent that is handed a `/name` it does
    /// not serve answers with its own confusion in the middle of a conversation,
    /// and the operator learns nothing about why. This names the command and
    /// what the agent actually offers.
    #[error(
        "assistant session {session_id} cannot run the command `{requested}`: the harness has not \
         advertised it. It advertises {advertised}. A command is offered only while the agent \
         itself lists it, so a command that has been withdrawn stops being runnable."
    )]
    UnknownCommand {
        /// The session the command was asked on.
        session_id: AssistantSessionId,
        /// The command name the caller sent.
        requested: String,
        /// What the harness advertises, rendered for the message.
        advertised: String,
    },

    /// A turn was submitted on a session that cannot be reopened.
    #[error("assistant session {session_id} has ended and cannot take another turn: {reason}")]
    Ended {
        /// The session that is over.
        session_id: AssistantSessionId,
        /// Why it is over.
        reason: String,
    },

    /// The harness could not be started, or its handshake failed.
    #[error("the assistant harness `{harness}` could not be started: {reason}")]
    HarnessFailed {
        /// The harness that failed.
        harness: String,
        /// The failure, in the harness's own words.
        reason: String,
    },

    /// The agent demands authentication that only the operator can supply, on
    /// the server host, out of band.
    #[error(
        "the assistant harness `{harness}` under account `{account}` is not logged in on the \
         server host: it answered `authentication required`. Log it in there (for example \
         `claude login` under that account's config directory); nothing in the console can enter \
         a credential, and this server never stores one."
    )]
    AuthRequired {
        /// The harness that needs logging in.
        harness: String,
        /// The account it needs logging in under — `default` when the harness
        /// declares no accounts.
        account: String,
    },

    /// The durable store refused or failed.
    #[error("the assistant session store failed: {0}")]
    Store(#[from] StoreError),

    /// Something the server itself could not do — encode a frame, resolve its
    /// own address. Never a caller's fault, and never silent.
    #[error("the assistant session surface failed internally: {0}")]
    Internal(String),
}

impl AssistantSessionError {
    /// Whether this refusal must be reported to the caller as a plain
    /// not-found, whatever it really was.
    ///
    /// A session belonging to another subject is not found AS FAR AS THIS
    /// CALLER IS CONCERNED: answering "forbidden" would confirm that a session
    /// with that id exists, which is exactly the existence leak the namespace
    /// boundary refuses elsewhere.
    #[must_use]
    pub const fn is_not_found(&self) -> bool {
        matches!(self, Self::NotFound { .. } | Self::NotYours { .. })
    }

    /// The stable word this refusal is reported to a client under.
    ///
    /// ONE mint, so a refusal that arrives as a `turn_failed` frame carries the
    /// same word the HTTP answer carries — a console branching on
    /// `harness_unavailable` must not have to know which transport told it. The
    /// match is exhaustive: a variant added to this enum has to choose a word
    /// here before it can reach anybody.
    #[must_use]
    pub const fn code(&self) -> &'static str {
        match self {
            Self::NotCommissioned { .. } => "not_commissioned",
            Self::UnknownHarness { .. } => "unknown_harness",
            Self::HarnessUnavailable { .. } => "harness_unavailable",
            Self::AccountEnvironmentAbsent { .. } => "account_environment_absent",
            Self::UnknownAccount { .. } => "unknown_account",
            Self::NotFound { .. } | Self::NotYours { .. } => "not_found",
            Self::Busy { .. } => "busy",
            Self::UnknownCommand { .. } => "unknown_command",
            Self::Ended { .. } => "ended",
            Self::HarnessFailed { .. } => "harness_failed",
            Self::AuthRequired { .. } => "auth_required",
            Self::Store(_) => "store_failed",
            Self::Internal(_) => "internal",
        }
    }
}