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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
//! The served assistant-session surface.
//!
//! Two things are pinned here that nothing else can pin:
//!
//! - the STATUS MAPPING is total. Every [`AssistantSessionError`] variant is
//!   constructed and mapped, and the expected status is itself decided by an
//!   exhaustive match — so a variant added to the surface fails to compile in
//!   this file until somebody chooses what a caller should be told;
//! - the DESCRIPTOR walks the grant grammar rather than copying it, so a word
//!   added to `namespace/grants.rs` appears to the console without a second
//!   edit, and fails here if it does not.
//!
//! Every assertion reads the BODY. The ops-console catch-all answers
//! `200 text/html` for any path a binary does not serve, so a status-only probe
//! cannot tell a build that carries these routes from one that does not.

use std::collections::BTreeMap;
use std::sync::Arc;

use aion::EngineBuilder;
use aion_core::AssistantSessionId;
use aion_store::{EventStore, InMemoryStore, StoreError};
use axum::http::StatusCode;
use serde_json::Value;
use tower::ServiceExt;

use super::super::router::workflow_router;
use super::super::test_support::{
    get_request, json_request, read_json, runtime_config, server_state,
};
use super::session_refusal;
use aion_integration_acp::catalogue;

use crate::assistant::sessions::AssistantSessionError;
use crate::config::{
    AssistantConfig, AssistantHarnessConfig, NamespaceMode, ResolvedAssistantConfig,
};
use crate::namespace::grants::GRANT_WORDS;
use crate::test_support::{EngineUnderTest, StateUnderTest};
use crate::{NamespaceResolver, StaticScheduleNamespaces, StaticWorkflowNamespaces};

type TestResult = Result<(), Box<dyn std::error::Error>>;

/// State whose server carries `assistant` as its resolved `[assistant]`
/// section, held by the caller so the engine is shut down when the test ends;
/// `workflow_router(state.clone())` is the router.
async fn assistant_state(
    assistant: ResolvedAssistantConfig,
) -> Result<StateUnderTest, Box<dyn std::error::Error>> {
    let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
    let engine = EngineUnderTest::new(Arc::new(
        EngineBuilder::new()
            .stop_drain_timeout(std::time::Duration::from_secs(5))
            .store_arc(store)
            .in_memory_visibility()
            .scheduler_threads(1)
            .build()
            .await?,
    ));
    let resolver = NamespaceResolver::from_parts(
        NamespaceMode::SharedEngine,
        Some(engine.handle()),
        Arc::new(StaticWorkflowNamespaces::default()),
        Arc::new(StaticScheduleNamespaces::default()),
    );
    let mut config = runtime_config();
    // Auth off puts the request on the single-tenant operator path, which holds
    // every grant word — so these tests measure the ROUTES rather than a grant
    // refusal standing in front of all of them.
    config.auth.enabled = false;
    config.assistant = assistant;
    server_state(engine, resolver, config).await
}

/// The STOCK section: no `[assistant]` at all, which is what a fresh install
/// has — and which still serves the assistant.
fn stock_assistant() -> ResolvedAssistantConfig {
    ResolvedAssistantConfig::default()
}

/// A section declaring one account on a catalogue harness, resolved through the
/// real config resolver rather than hand-built — so what these tests describe is
/// what an operator's file would actually produce.
///
/// This is the WHOLE of what the section can still say: the launch line, the
/// working directory and every timeout are the catalogue's or are gone.
fn assistant_with_an_account() -> ResolvedAssistantConfig {
    AssistantConfig {
        harnesses: vec![AssistantHarnessConfig {
            name: Some(CATALOGUE_HARNESS.to_owned()),
            accounts: vec![crate::config::AssistantAccountConfig {
                name: Some("work".to_owned()),
                env: BTreeMap::from([(
                    "CLAUDE_CONFIG_DIR".to_owned(),
                    "AION_CLAUDE_WORK_DIR".to_owned(),
                )]),
            }],
        }],
    }
    .resolved()
}

/// The catalogue entry these cells declare accounts on.
const CATALOGUE_HARNESS: &str = "claude-code";

/// A `{id}` that is not a session id is a `400` NAMING the text, not a 404 and
/// not a 500: the caller sent something that is not an identifier, and the only
/// way they can fix it is to be told which text was rejected.
#[tokio::test]
async fn an_unparseable_session_id_is_a_400_naming_the_text() -> TestResult {
    let state = assistant_state(stock_assistant()).await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant/sessions/not-a-session-id")?)
        .await?;
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body: Value = read_json(response).await?;
    let message = body["message"].as_str().unwrap_or_default().to_owned();
    assert!(
        message.contains("not-a-session-id"),
        "the refusal must name the text that was rejected: {body}"
    );
    Ok(())
}

/// A well-formed id that names nothing is a `404`. The same answer covers a
/// session belonging to another subject — deliberately, so nobody learns from a
/// status code that a session they cannot see exists.
#[tokio::test]
async fn a_session_id_that_names_nothing_is_a_404() -> TestResult {
    let absent = AssistantSessionId::new(uuid::Uuid::from_u128(0x5e5_5107));
    let state = assistant_state(stock_assistant()).await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request(&format!("/assistant/sessions/{absent}"))?)
        .await?;
    assert_eq!(response.status(), StatusCode::NOT_FOUND);
    Ok(())
}

/// `current` is a ROUTE, not a session id.
///
/// It shares a prefix with `/assistant/sessions/{id}`, so the discriminating
/// read is not that the status is 404 — an id parse failure is a 400, and a
/// parsed-but-absent id is also a 404. It is that the body says the CALLER has
/// no current session rather than that `current` is not a UUID: the first means
/// the literal route answered, the second means the capture swallowed it.
#[tokio::test]
async fn current_is_a_route_rather_than_a_session_id() -> TestResult {
    let state = assistant_state(stock_assistant()).await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant/sessions/current")?)
        .await?;
    assert_eq!(
        response.status(),
        StatusCode::NOT_FOUND,
        "a caller with no session has no current one"
    );
    let body: Value = read_json(response).await?;
    let message = body["message"].as_str().unwrap_or_default().to_owned();
    assert!(
        !message.contains("is not an assistant session id"),
        "`current` was parsed as a session id, so the literal route never ran: {body}"
    );
    assert!(
        message.contains("no assistant session"),
        "the refusal must say the caller holds no current session: {body}"
    );
    Ok(())
}

/// With no `[assistant]` section the descriptor says so IN WORDS, and the one
/// route that cannot work without a harness refuses with `503`.
///
/// O1 + O2: a STOCK server — no `[assistant]` section at all — serves the
/// assistant, and its `harnesses[]` IS the catalogue this build ships.
///
/// This is the amendment's first pin, and it is a whole-surface read rather than
/// one flag: `sessions_enabled` true, `sessions_disabled_reason` null (nothing
/// is "not configured" any more), every catalogue entry published with the exact
/// line the server would run, and a hint present exactly when the entry cannot
/// run here.
#[tokio::test]
async fn a_stock_server_serves_the_assistant_and_publishes_the_whole_catalogue() -> TestResult {
    let state = assistant_state(stock_assistant()).await?;
    let described = workflow_router(state.clone())
        .oneshot(get_request("/assistant")?)
        .await?;
    assert_eq!(described.status(), StatusCode::OK);
    let body: Value = read_json(described).await?;
    assert_eq!(
        body["sessions_enabled"],
        serde_json::json!(true),
        "a server with no [assistant] section must still serve the assistant: {body}"
    );
    assert!(
        body["sessions_disabled_reason"].is_null(),
        "`not configured` is not a reason any more; the field is for a fault: {body}"
    );
    assert!(
        body["default_harness"].is_null(),
        "a caller who has opened nothing has picked nothing: {body}"
    );
    let harnesses = body["harnesses"]
        .as_array()
        .ok_or("the description must carry a `harnesses` array")?;
    assert!(
        harnesses.len() >= 5,
        "the catalogue ships Claude Code, Codex, Gemini CLI, OpenCode and Pi: {body}"
    );
    for (published, entry) in harnesses.iter().zip(catalogue::CATALOGUE) {
        assert_eq!(published["name"], serde_json::json!(entry.id));
        assert_eq!(published["kind"], serde_json::json!("acp"));
        assert_eq!(
            published["launch"],
            serde_json::json!(entry.launch()),
            "the published line must be the catalogue's own, so what an operator reads is what \
             the server runs: {published}"
        );
        assert_eq!(
            published["accounts"],
            serde_json::json!([]),
            "a stock server declares no account: {published}"
        );
        // The hint is present exactly when the entry cannot run here, so a
        // console never renders "install Node.js" beside a working harness and
        // never leaves an operator with a disabled row and no remedy.
        let available = published["available"]
            .as_bool()
            .ok_or("every entry states whether it is available")?;
        assert_eq!(
            available,
            entry.available(),
            "`{}` is published as available: {available}, measured: {}",
            entry.id,
            entry.available()
        );
        if available {
            assert!(
                published["install_hint"].is_null(),
                "an available harness needs no install hint: {published}"
            );
        } else {
            assert_eq!(
                published["install_hint"],
                serde_json::json!(entry.install_hint),
                "an unavailable harness carries the catalogue's own sentence: {published}"
            );
        }
    }
    Ok(())
}

/// The accounts a section declares hang off the CATALOGUE entry they name, and
/// nothing else about that entry comes from the file.
///
/// The positive control for the stock cell above: without it, every assertion
/// there would pass on a descriptor that published an empty account list
/// unconditionally.
#[tokio::test]
async fn a_declared_account_is_published_on_the_catalogue_harness_it_names() -> TestResult {
    let state = assistant_state(assistant_with_an_account()).await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant")?)
        .await?;
    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = read_json(response).await?;
    let harnesses = body["harnesses"]
        .as_array()
        .ok_or("the description must carry a `harnesses` array")?;
    let declared = harnesses
        .iter()
        .find(|entry| entry["name"] == serde_json::json!(CATALOGUE_HARNESS))
        .ok_or("the catalogue entry the section names must still be published")?;
    assert_eq!(
        declared["accounts"],
        serde_json::json!(["work"]),
        "a harness's declared accounts are what a client offers: {body}"
    );
    let others: Vec<&Value> = harnesses
        .iter()
        .filter(|entry| entry["name"] != serde_json::json!(CATALOGUE_HARNESS))
        .collect();
    assert!(
        others
            .iter()
            .all(|entry| entry["accounts"] == serde_json::json!([])),
        "declaring an account on one harness must not put it on the others: {body}"
    );
    Ok(())
}

/// O4: `createSession` answers the availability question at SELECTION time,
/// with the same refusal a first message would have produced.
///
/// Both arms, decided by the measurement rather than by a guess about the venue:
/// an entry this machine can run opens a session, and one it cannot is refused
/// with the launch line and the install hint — never accepted and failed later,
/// which would put the operator's first message in the position of discovering
/// that the harness they picked was never startable.
#[tokio::test]
async fn creating_a_session_on_a_harness_this_machine_cannot_run_is_refused_with_the_hint()
-> TestResult {
    for entry in catalogue::CATALOGUE {
        let state = assistant_state(stock_assistant()).await?;
        let response = workflow_router(state.clone())
            .oneshot(json_request(
                "/assistant/sessions",
                &serde_json::json!({ "harness": entry.id, "account": null, "title": null }),
            )?)
            .await?;
        let status = response.status();
        let body: Value = read_json(response).await?;
        if entry.available() {
            assert_eq!(
                status,
                StatusCode::CREATED,
                "`{}` resolves on this server's PATH, so a session opens on it: {body}",
                entry.id
            );
            assert_eq!(body["harness"], serde_json::json!(entry.id));
        } else {
            assert_eq!(
                status,
                StatusCode::SERVICE_UNAVAILABLE,
                "`{}` does not resolve on this server's PATH, so the session must be refused \
                 NOW: {body}",
                entry.id
            );
            let message = body["message"].as_str().unwrap_or_default();
            assert!(
                message.contains(&entry.launch()),
                "the refusal names the line that could not be run: {body}"
            );
            assert!(
                message.contains(entry.install_hint),
                "the refusal carries the catalogue's install hint: {body}"
            );
        }
    }
    Ok(())
}

/// O3: `default_harness` is the caller's LAST PICK, written by createSession.
///
/// Before any pick it is null (pinned in the stock cell above); after one it is
/// what the operator opened. Read back through the DESCRIPTOR, because that is
/// the field the console preselects from — a memory that existed in the store
/// and never reached the description would preselect nothing.
#[tokio::test]
async fn the_descriptor_reports_the_harness_this_caller_last_opened() -> TestResult {
    let Some(available) = catalogue::CATALOGUE.iter().find(|entry| entry.available()) else {
        // This venue can run no harness at all, so no pick can be made through
        // the door that makes one. The store-level half of this requirement is
        // pinned backend-independently by
        // `the_last_pick_is_per_caller_and_replaces_itself` in the assistant
        // conformance suite, and its restart arm beside it.
        tracing::info!(
            "skipping: no catalogue harness resolves on this venue's PATH, so no session can be \
             created to record a pick"
        );
        return Ok(());
    };
    // ONE router, so both requests are the same server and the same caller.
    let state = assistant_state(stock_assistant()).await?;
    let router = workflow_router(state.clone());
    let created = router
        .clone()
        .oneshot(json_request(
            "/assistant/sessions",
            &serde_json::json!({ "harness": available.id, "account": null, "title": null }),
        )?)
        .await?;
    assert_eq!(created.status(), StatusCode::CREATED);
    let described = router.oneshot(get_request("/assistant")?).await?;
    assert_eq!(described.status(), StatusCode::OK);
    let body: Value = read_json(described).await?;
    assert_eq!(
        body["default_harness"],
        serde_json::json!(available.id),
        "the descriptor must report what this caller last opened: {body}"
    );
    Ok(())
}

/// Requirement (c): the descriptor names `assistant_context`, the route it is
/// served on, and the KIND of credential that reaches it — and it says
/// server-minted and session-scoped in words.
///
/// Published because an operator has no other way to see that opening a session
/// hands its agent a tool that can read the console screen, or what authorizes
/// that tool. A capability an operator cannot discover is one they cannot
/// consent to.
#[tokio::test]
async fn the_descriptor_names_the_assistant_tool_and_the_credential_that_reaches_it() -> TestResult
{
    let state = assistant_state(assistant_with_an_account()).await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant")?)
        .await?;
    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = read_json(response).await?;
    let assistant = &body["tools"]["assistant"];
    assert_eq!(
        assistant["tools"],
        serde_json::json!(crate::assistant::mcp::SESSION_TOOL_NAMES),
        "the assistant tool server's catalogue must be published by name, every session tool in \
         catalogue order: {body}"
    );
    assert_eq!(
        assistant["route"],
        serde_json::json!(crate::assistant::mcp::ASSISTANT_MCP_PATH),
        "the route the tool is served on must be published: {body}"
    );
    assert_eq!(assistant["server"], serde_json::json!("assistant"));

    let token = &assistant["token"];
    assert_eq!(
        token["kind"],
        serde_json::json!(crate::assistant::mcp::SESSION_TOKEN_KIND)
    );
    assert_eq!(token["minted_by"], serde_json::json!("server"));
    assert_eq!(token["scope"], serde_json::json!("session"));
    let described = token["description"]
        .as_str()
        .ok_or("the credential must carry its description")?;
    assert_eq!(
        described,
        crate::assistant::mcp::SESSION_TOKEN_DESCRIPTION,
        "ONE sentence describes this credential; the descriptor must quote it, not restate it"
    );
    // And it must actually say the two things the requirement names, so a
    // rewritten sentence that dropped them fails here.
    assert!(
        described.contains("server-minted") && described.contains("session-scoped"),
        "the descriptor's sentence must say server-minted and session-scoped: {described}"
    );
    Ok(())
}

/// The server name the description publishes is the one a SPAWN hands over.
///
/// Two places know this name — the descriptor and `launch.rs` — so the second
/// is pinned against the first. A description naming a server the agent never
/// receives would send an operator looking for tools that are not there.
#[test]
fn the_published_assistant_server_name_is_the_one_a_spawn_hands_over() {
    assert_eq!(
        crate::assistant::sessions::launch::ASSISTANT_MCP_SERVER_NAME,
        "assistant",
        "the descriptor publishes this name; a spawn must hand over the same one"
    );
}

/// The published `kind` is the one every catalogue entry runs.
///
/// The catalogue is an ACP catalogue by construction — this crate's adapter is
/// the only thing that reads it — so the descriptor publishes a constant, and
/// the constant is pinned here against the id of what a session actually
/// resolves to. The other half, that a name outside the catalogue is refused at
/// config load naming what IS shipped, is
/// `a_harness_name_the_build_does_not_ship_is_refused_naming_the_catalogue` in
/// `config/assistant_tests.rs`.
#[test]
fn every_published_harness_is_one_the_adapter_can_actually_run() {
    assert!(
        !catalogue::CATALOGUE.is_empty(),
        "a build that shipped an empty catalogue would publish an assistant nobody can start"
    );
    for entry in catalogue::CATALOGUE {
        assert!(
            catalogue::harness(entry.id).is_some(),
            "`{}` is published but does not resolve through the one lookup a create request uses",
            entry.id
        );
        assert!(
            !entry.launch().trim().is_empty(),
            "`{}` publishes an empty launch line",
            entry.id
        );
    }
}

/// THE VOCABULARY PIN for the assistant descriptor. Every word in
/// `GRANT_WORDS` appears in `grants` with the grammar's own description.
///
/// It walks the table itself, never a copy: the console must not invent a grant
/// word's meaning, so the server states it, and a word added to the grammar
/// without reaching this description would be enforceable and undiscoverable
/// from the one screen that asks the operator to hold it.
#[tokio::test]
async fn the_descriptor_lists_every_word_in_the_grant_vocabulary() -> TestResult {
    // Vacuity control: an emptied vocabulary would satisfy the loop below.
    assert!(
        !GRANT_WORDS.is_empty(),
        "the grant vocabulary is empty, so this pin measures nothing"
    );
    let state = assistant_state(stock_assistant()).await?;
    let response = workflow_router(state.clone())
        .oneshot(get_request("/assistant")?)
        .await?;
    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = read_json(response).await?;
    let listed = body["grants"]
        .as_array()
        .ok_or("the description must carry a `grants` array")?;
    assert_eq!(
        listed.len(),
        GRANT_WORDS.len(),
        "`grants` lists {} words for a vocabulary of {}: {body}",
        listed.len(),
        GRANT_WORDS.len()
    );
    for grant in GRANT_WORDS {
        let described = listed
            .iter()
            .find(|row| row["name"] == serde_json::json!(grant.word()))
            .ok_or_else(|| {
                format!(
                    "`{}` is in the grant vocabulary but not in the assistant description",
                    grant.word()
                )
            })?;
        assert_eq!(
            described["description"],
            serde_json::json!(grant.description()),
            "`{}` is described with words the grammar does not carry",
            grant.word()
        );
        assert!(
            !grant.description().trim().is_empty(),
            "`{}` reaches the console with nothing to explain it",
            grant.word()
        );
        assert_eq!(
            described["held"],
            serde_json::json!(true),
            "the auth-off operator must hold `{}`",
            grant.word()
        );
    }
    Ok(())
}

/// EVERY refusal this surface can produce has a status, and it is the right
/// one.
///
/// The census is enforced by the compiler twice over: [`session_refusal`]
/// matches exhaustively, and so does [`expected_status`] below. A variant added
/// to [`AssistantSessionError`] therefore cannot reach a client without
/// somebody deciding, in this file, what a caller should be told about it.
#[test]
fn every_refusal_variant_carries_a_status() {
    let census = every_variant();
    let mut seen = Vec::with_capacity(census.len());
    for error in &census {
        let discriminant = std::mem::discriminant(error);
        assert!(
            !seen.contains(&discriminant),
            "the census lists one variant twice, so it covers fewer than it appears to: {error}"
        );
        seen.push(discriminant);

        let (status, wire) = session_refusal(error);
        assert_eq!(
            status,
            expected_status(error),
            "`{error}` is answered with the wrong status"
        );
        assert!(
            !wire.message.trim().is_empty(),
            "`{error}` maps to a refusal with no message"
        );
        assert!(
            wire.error_type.is_some(),
            "`{error}` maps to a refusal a client cannot branch on"
        );
    }
}

/// A session belonging to ANOTHER subject is a plain not-found on the wire, and
/// its message must not carry the owning subject.
///
/// The server's own audit line keeps the distinction; the caller must not be
/// able to tell "no such session" from "not yours", or the id space becomes an
/// oracle for whether another operator's conversation exists.
#[test]
fn another_subjects_session_is_indistinguishable_from_an_absent_one() {
    let session_id = AssistantSessionId::new(uuid::Uuid::from_u128(0xf00d));
    let (absent_status, absent) = session_refusal(&AssistantSessionError::NotFound { session_id });
    let (denied_status, denied) = session_refusal(&AssistantSessionError::NotYours {
        session_id,
        subject: "someone-else".to_owned(),
    });

    assert_eq!(absent_status, StatusCode::NOT_FOUND);
    assert_eq!(denied_status, StatusCode::NOT_FOUND);
    assert_eq!(
        absent.message, denied.message,
        "the two answers differ, so the id space tells a caller which sessions exist"
    );
    assert_eq!(absent.error_type, denied.error_type);
    assert!(
        !denied.message.contains("someone-else"),
        "the refusal names the owning subject: {}",
        denied.message
    );
}

/// One of every [`AssistantSessionError`] variant.
///
/// Hand-listed, because a value cannot be conjured from a discriminant — the
/// exhaustive match in [`expected_status`] is what makes forgetting one a
/// compile error rather than a silent gap.
fn every_variant() -> Vec<AssistantSessionError> {
    let session_id = AssistantSessionId::new(uuid::Uuid::from_u128(1));
    vec![
        AssistantSessionError::NotCommissioned {
            reason: "the store could not be read".to_owned(),
        },
        AssistantSessionError::UnknownHarness {
            requested: "ghost".to_owned(),
            declared: "claude-code, codex".to_owned(),
        },
        AssistantSessionError::HarnessUnavailable {
            harness: "opencode".to_owned(),
            launch: "opencode acp".to_owned(),
            install_hint: "install OpenCode".to_owned(),
        },
        AssistantSessionError::AccountEnvironmentAbsent {
            harness: "claude-code".to_owned(),
            account: "work".to_owned(),
            variables: "`AION_CLAUDE_WORK_DIR`".to_owned(),
        },
        AssistantSessionError::UnknownAccount {
            harness: "acme".to_owned(),
            requested: "ghost".to_owned(),
            declared: "work".to_owned(),
        },
        AssistantSessionError::NotFound { session_id },
        AssistantSessionError::NotYours {
            session_id,
            subject: "alice".to_owned(),
        },
        AssistantSessionError::UnknownCommand {
            session_id,
            requested: "compact".to_owned(),
            advertised: "none".to_owned(),
        },
        AssistantSessionError::UnknownConfigOption {
            session_id,
            requested: "model".to_owned(),
            advertised: "none".to_owned(),
        },
        AssistantSessionError::Busy { session_id },
        AssistantSessionError::Ended {
            session_id,
            reason: "the agent exited".to_owned(),
        },
        AssistantSessionError::HarnessFailed {
            harness: "acme".to_owned(),
            reason: "the binary is not executable".to_owned(),
        },
        AssistantSessionError::AuthRequired {
            harness: "acme".to_owned(),
            account: "work".to_owned(),
        },
        AssistantSessionError::Store(StoreError::Backend("disk is gone".to_owned())),
        AssistantSessionError::Internal("a frame is not encodable".to_owned()),
    ]
}

/// What each variant must be answered with, stated independently of the
/// mapping under test and exhaustively, so a new variant stops the build here.
fn expected_status(error: &AssistantSessionError) -> StatusCode {
    match error {
        AssistantSessionError::NotFound { .. } | AssistantSessionError::NotYours { .. } => {
            StatusCode::NOT_FOUND
        }
        AssistantSessionError::Busy { .. } | AssistantSessionError::Ended { .. } => {
            StatusCode::CONFLICT
        }
        AssistantSessionError::UnknownHarness { .. }
        | AssistantSessionError::UnknownAccount { .. }
        | AssistantSessionError::UnknownCommand { .. }
        | AssistantSessionError::UnknownConfigOption { .. } => StatusCode::BAD_REQUEST,
        AssistantSessionError::NotCommissioned { .. }
        // A harness this machine cannot run, and an account whose value this
        // machine does not carry, are both facts about the SERVER that no
        // request could avoid — the same 503 an unusable store answers with.
        | AssistantSessionError::HarnessUnavailable { .. }
        | AssistantSessionError::AccountEnvironmentAbsent { .. } => StatusCode::SERVICE_UNAVAILABLE,
        AssistantSessionError::AuthRequired { .. }
        | AssistantSessionError::HarnessFailed { .. } => StatusCode::BAD_GATEWAY,
        AssistantSessionError::Store(_) | AssistantSessionError::Internal(_) => {
            StatusCode::INTERNAL_SERVER_ERROR
        }
    }
}