aion-server 0.31.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
//! What a spawn is composed of, what it must never carry, and when it refuses.
//!
//! # Availability is measured, so these cells read the measurement
//!
//! Whether `npx` or `opencode` is on this box is a venue fact, and a cell that
//! assumed either way would be unread in half the venues it runs in. So the
//! catalogue cells below take the measurement and assert the CONSEQUENCE:
//! an entry that resolves must plan, an entry that does not must refuse with the
//! catalogue's own launch line and install hint. Whatever this machine carries,
//! at least one arm runs, and the claim — the refusal is minted from the
//! measurement, never from a remembered flag — is the same either way.
//!
//! The other half of that pin, that the measurement itself moves when the `PATH`
//! moves, is hermetic and lives where it can plant a `PATH`:
//! `aion_integration_acp::catalogue`'s
//! `availability_is_measured_against_the_path_it_is_asked_about`.

use aion_core::AssistantSessionId;
use aion_integration_acp::catalogue::{self, CatalogueHarness};

use crate::config::ResolvedAssistantAccount;

use super::*;

/// The catalogue entry the composition cells are built on.
fn harness() -> Result<&'static CatalogueHarness, String> {
    catalogue::harness("claude-code")
        .ok_or_else(|| "the catalogue must ship the claude-code entry".to_owned())
}

/// An account whose value comes from a variable this test process really has.
///
/// `PATH` is the one variable every venue carries, so the account resolves here
/// without the test setting anything — which it may not do: mutating a process's
/// environment is unsafe in this edition, and a cell that needed to would be a
/// cell that could not run.
fn account() -> ResolvedAssistantAccount {
    ResolvedAssistantAccount {
        name: "work".to_owned(),
        env: vec![("CLAUDE_CONFIG_DIR".to_owned(), "PATH".to_owned())],
    }
}

/// An account naming a source variable no environment carries.
fn account_with_absent_source() -> ResolvedAssistantAccount {
    ResolvedAssistantAccount {
        name: "work".to_owned(),
        env: vec![(
            "CLAUDE_CONFIG_DIR".to_owned(),
            "AION_ASSISTANT_ABSENT_SOURCE_FOR_THIS_PIN".to_owned(),
        )],
    }
}

/// A server that can state an address AND mounts the general workflow tools.
fn endpoint() -> AssistantEndpoints {
    AssistantEndpoints {
        base: "http://127.0.0.1:8080".to_owned(),
        aion_mcp_enabled: true,
    }
}

/// A server that can state an address and serves NO general workflow tools —
/// `[mcp] enabled = false`. Its assistant route is still there.
fn endpoint_with_general_mcp_dark() -> AssistantEndpoints {
    AssistantEndpoints {
        base: "http://127.0.0.1:8080".to_owned(),
        aion_mcp_enabled: false,
    }
}

/// One HTTP server's parts as a test reads them: its URL and its headers.
type HttpServerParts<'specs> = (&'specs str, &'specs [(String, String)]);

/// The HTTP server handed over under `name`, or a failure naming what was.
fn http_server<'specs>(
    specs: &'specs [McpServerSpec],
    name: &str,
) -> Result<HttpServerParts<'specs>, String> {
    specs
        .iter()
        .find_map(|spec| match spec {
            McpServerSpec::Http {
                name: served,
                url,
                headers,
            } if served == name => Some((url.as_str(), headers.as_slice())),
            _ => None,
        })
        .ok_or_else(|| format!("no MCP server named `{name}` was handed over, got {specs:?}"))
}

/// Plan on `entry`, or report the refusal a venue without it produces.
///
/// The one place these cells decide whether the machine can run a harness, so
/// the branch reads the same way everywhere.
fn plan_if_available(
    entry: &'static CatalogueHarness,
    account: Option<&ResolvedAssistantAccount>,
    endpoints: Option<&AssistantEndpoints>,
) -> Result<Option<HarnessPlan>, String> {
    match plan(AssistantSessionId::new_v4(), entry, account, endpoints) {
        Ok(plan) => Ok(Some(plan)),
        Err(AssistantSessionError::HarnessUnavailable { .. }) if !entry.available() => Ok(None),
        Err(error) => Err(format!("planning `{}` failed: {error}", entry.id)),
    }
}

#[test]
fn a_harness_this_machine_cannot_run_is_refused_with_its_line_and_its_hint() -> Result<(), String> {
    // Every entry, both arms, decided by the measurement rather than by a guess
    // about the venue: available entries must plan, unavailable ones must refuse
    // with the catalogue's own words. A build that shipped an entry with no hint
    // would fail here as surely as one that accepted a harness it cannot start.
    let mut planned = 0_usize;
    let mut refused = 0_usize;
    for entry in catalogue::CATALOGUE {
        match plan(AssistantSessionId::new_v4(), entry, None, None) {
            Ok(_plan) => {
                assert!(
                    entry.available(),
                    "`{}` planned while its launch program does not resolve on this server's \
                     PATH; the refusal would then arrive at the first message instead",
                    entry.id
                );
                planned = planned.saturating_add(1);
            }
            Err(AssistantSessionError::HarnessUnavailable {
                harness,
                launch,
                install_hint,
            }) => {
                assert!(
                    !entry.available(),
                    "`{}` was refused as unavailable while its program does resolve",
                    entry.id
                );
                assert_eq!(harness, entry.id);
                assert_eq!(
                    launch,
                    entry.launch(),
                    "the refusal states the exact line this server would have run"
                );
                assert_eq!(
                    install_hint, entry.install_hint,
                    "the refusal carries the catalogue's own install sentence, so an operator is \
                     told what to install rather than that something went wrong"
                );
                refused = refused.saturating_add(1);
            }
            Err(other) => {
                return Err(format!("`{}` failed for another reason: {other}", entry.id));
            }
        }
    }
    assert_eq!(
        planned.saturating_add(refused),
        catalogue::CATALOGUE.len(),
        "every catalogue entry is decided one way or the other"
    );
    Ok(())
}

#[test]
fn the_unavailable_refusal_is_the_same_shape_selection_and_spawn_both_use() {
    // O4's "never accepts-and-fails-later": `createSession` and the first turn
    // must produce ONE refusal, so the code a console branches on is minted in
    // one place and is the same word on both paths.
    let error = AssistantSessionError::HarnessUnavailable {
        harness: "opencode".to_owned(),
        launch: "opencode acp".to_owned(),
        install_hint: "install it".to_owned(),
    };
    assert_eq!(error.code(), "harness_unavailable");
    let rendered = error.to_string();
    for part in ["opencode", "opencode acp", "install it"] {
        assert!(
            rendered.contains(part),
            "the refusal must carry `{part}`: {rendered}"
        );
    }
}

#[test]
fn an_account_whose_source_variable_is_absent_is_a_typed_absence_not_an_empty_value()
-> Result<(), String> {
    let entry = harness()?;
    let absent = account_with_absent_source();
    match plan(AssistantSessionId::new_v4(), entry, Some(&absent), None) {
        Err(AssistantSessionError::AccountEnvironmentAbsent {
            harness,
            account,
            variables,
        }) => {
            assert_eq!(harness, entry.id);
            assert_eq!(account, "work");
            assert!(
                variables.contains("AION_ASSISTANT_ABSENT_SOURCE_FOR_THIS_PIN"),
                "the absence names the SERVER variable that is missing: {variables}"
            );
            assert!(
                variables.contains("CLAUDE_CONFIG_DIR"),
                "and the name the agent would have received, so the operator can see what the \
                 account was for: {variables}"
            );
            Ok(())
        }
        Err(AssistantSessionError::HarnessUnavailable { .. }) if !entry.available() => {
            // This venue cannot run the harness at all, and the availability
            // refusal comes first by design. The absence is still pinned by the
            // control below, which needs no harness.
            Ok(())
        }
        Err(other) => Err(format!("expected a typed absence, got: {other}")),
        Ok(_plan) => Err(
            "an account naming a variable this server does not carry must not spawn an agent with \
             an empty one: the harness then looks logged out and nothing says why"
                .to_owned(),
        ),
    }
}

#[test]
fn an_account_whose_source_variable_is_present_is_carried_under_the_name_the_child_expects()
-> Result<(), String> {
    let entry = harness()?;
    let account = account();
    let Some(plan) = plan_if_available(entry, Some(&account), None)? else {
        return Ok(());
    };
    // The positive control for the absence cell: the same code path, a source
    // that IS set, and the value reaches the child under the account's name.
    let rendered = format!("{:?}", plan.harness);
    assert!(
        rendered.contains("CLAUDE_CONFIG_DIR"),
        "the account's variable must reach the spawn under the name the harness reads: {rendered}"
    );
    Ok(())
}

#[test]
fn a_server_that_can_state_no_address_hands_over_nothing_and_mints_nothing() -> Result<(), String> {
    let entry = harness()?;
    let Some(plan) = plan_if_available(entry, None, None)? else {
        return Ok(());
    };
    assert!(
        plan.token.is_none(),
        "a session handed no endpoint of ours needs no identity of its own"
    );
    assert!(plan.harness.mcp_servers().is_empty());
    Ok(())
}

/// TWO specs, not one merged catalogue: the general workflow tools and the
/// assistant's own context tool are separate servers on separate routes, and
/// both carry the SAME session bearer because both are this server being told
/// "I am session X's agent".
#[test]
fn both_of_this_servers_endpoints_are_handed_over_with_one_session_scoped_bearer()
-> Result<(), String> {
    let entry = harness()?;
    let session_id = AssistantSessionId::new_v4();
    let plan = match plan(session_id, entry, None, Some(&endpoint())) {
        Ok(plan) => plan,
        Err(AssistantSessionError::HarnessUnavailable { .. }) if !entry.available() => {
            return Ok(());
        }
        Err(error) => return Err(error.to_string()),
    };
    let minted = plan
        .token
        .as_ref()
        .ok_or_else(|| "a session handed an endpoint is given an identity".to_owned())?;
    let specs = plan.harness.mcp_servers();
    assert_eq!(specs.len(), 2, "two servers of ours, got {specs:?}");

    let (aion_url, aion_headers) = http_server(specs, AION_MCP_SERVER_NAME)?;
    assert_eq!(aion_url, "http://127.0.0.1:8080/mcp");
    let (assistant_url, assistant_headers) = http_server(specs, ASSISTANT_MCP_SERVER_NAME)?;
    assert_eq!(
        assistant_url, "http://127.0.0.1:8080/assistant/mcp",
        "the assistant tools are on their OWN route, never the general one"
    );

    for (surface, headers) in [("aion", aion_headers), ("assistant", assistant_headers)] {
        let authorization = headers
            .iter()
            .find(|(key, _value)| key == "authorization")
            .ok_or_else(|| format!("the {surface} endpoint carries a bearer"))?;
        assert!(
            authorization.1.ends_with(minted.secret()),
            "the {surface} endpoint must carry the session's own minted bearer"
        );
        let session = headers
            .iter()
            .find(|(key, _value)| key == token::SESSION_ID_HEADER)
            .ok_or_else(|| format!("the {surface} endpoint names the session it belongs to"))?;
        assert_eq!(session.1, session_id.to_string());
    }
    Ok(())
}

/// `[mcp] enabled` governs the WORKFLOW tools. Darkening them must not take
/// the assistant's own context tool away from its own agent — that would leave
/// the agent asking the operator which file is open, which is the one behaviour
/// this surface exists to remove.
#[test]
fn the_assistant_tool_server_is_handed_over_even_when_the_general_mcp_is_dark() -> Result<(), String>
{
    let entry = harness()?;
    let Some(plan) = plan_if_available(entry, None, Some(&endpoint_with_general_mcp_dark()))?
    else {
        return Ok(());
    };
    let specs = plan.harness.mcp_servers();
    assert_eq!(specs.len(), 1, "only the assistant server, got {specs:?}");
    let (url, _headers) = http_server(specs, ASSISTANT_MCP_SERVER_NAME)?;
    assert_eq!(url, "http://127.0.0.1:8080/assistant/mcp");
    assert!(
        plan.token.is_some(),
        "the assistant route is bearer-only, so a session reaching it must be given a bearer"
    );
    // The negative control: the general server is genuinely absent, so the
    // assertion above is not passing on a two-server plan.
    assert!(
        http_server(specs, AION_MCP_SERVER_NAME).is_err(),
        "the general workflow tools must NOT be handed over while `[mcp]` is dark"
    );
    Ok(())
}

/// The bearer leaves the mint exactly twice — into the two MCP specifications —
/// and reaches no rendering of anything. This is the pin the "never logged"
/// requirement rests on for the CREDENTIAL, as its sibling below is for an
/// account's values.
#[test]
fn the_session_bearer_never_reaches_a_rendering_of_the_plan() -> Result<(), String> {
    let entry = harness()?;
    let Some(plan) = plan_if_available(entry, None, Some(&endpoint()))? else {
        return Ok(());
    };
    let secret = plan
        .token
        .as_ref()
        .ok_or_else(|| "the plan mints a bearer".to_owned())?
        .secret()
        .to_owned();
    // The control: the secret really is on the wire the child reads, so the
    // absence below is a rendering discipline rather than an unminted token.
    let specs = format!("{:?}", plan.harness.mcp_servers());
    assert!(
        specs.contains(&secret),
        "the bearer must reach the child's MCP specification"
    );
    for rendered in [format!("{plan:?}"), format!("{:?}", plan.token)] {
        assert!(
            !rendered.contains(&secret),
            "the session bearer must not reach a rendering: {rendered}"
        );
    }
    Ok(())
}

#[test]
fn an_accounts_values_never_reach_a_rendering_of_the_plan() -> Result<(), String> {
    let entry = harness()?;
    let account = account();
    let Some(plan) = plan_if_available(entry, Some(&account), None)? else {
        return Ok(());
    };
    // The account's VALUE here is this process's own PATH, which is exactly the
    // kind of thing that must not be printed: the pairs are placed on the
    // Command, which is what the child reads, and the plan's own Debug is what
    // a trace field or an error chain would print.
    let value =
        std::env::var("PATH").map_err(|error| format!("this venue has no PATH: {error}"))?;
    let rendered = format!("{plan:?}");
    assert!(
        !rendered.contains(&value),
        "an account's value must not reach a rendering of the plan: {rendered}"
    );
    Ok(())
}

#[test]
fn the_agent_environment_is_a_stated_allow_list_that_carries_what_a_launch_needs() {
    // The knob is gone, so the set is a constant — and a constant that dropped
    // PATH or HOME would produce an agent that cannot exec `npx` or find its own
    // login state, which is the failure this cell exists to catch. Read off the
    // constant rather than off a spawn, because a spawn's environment is not
    // rendered anywhere by design.
    for required in ["PATH", "HOME"] {
        assert!(
            AGENT_ENVIRONMENT.contains(&required),
            "the assistant's agent environment must carry {required}"
        );
    }
    assert!(
        !AGENT_ENVIRONMENT.iter().any(|name| name.contains('=')),
        "the set is NAMES, never NAME=VALUE pairs"
    );
}

impl std::fmt::Debug for HarnessPlan {
    /// Prints what the plan IS, never what it carries.
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("HarnessPlan")
            .field("mcp_servers", &self.harness.mcp_servers().len())
            .field("token", &self.token)
            .finish()
    }
}