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
//! 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",
}
}
}