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
//! Composing the ACP harness one session runs on.
//!
//! Everything a spawn needs is decided here, once, from the CATALOGUE entry the
//! operator picked plus the session's own identity — so no other module builds a
//! `Command`, and the facts that are easy to get wrong are in one place with
//! their reasons beside them.
//!
//! # Nothing here is configuration any more
//!
//! The launch line is the catalogue's (`aion_integration_acp::catalogue`), which
//! is what makes a fresh install usable: no command, no arguments, no working
//! directory, no timeouts. What is left are three product decisions, each stated
//! out loud below rather than defaulted quietly — the environment the child
//! gets, where it is rooted, and how its permission requests are answered.
//!
//! # The child's environment is CONSTRUCTED
//!
//! `env_clear()` then exactly [`AGENT_ENVIRONMENT`] resolved out of the server's
//! own environment, plus the account's declared variables. A variable nobody
//! named cannot reach the agent whatever this server carries — the rule that
//! exists because one agent's own shell variable once reached a nested
//! `claude-code-acp` and made it refuse every `session/new`.
//!
//! An account names its variables on BOTH sides (the name the child gets, the
//! name it is read from), so no value is ever written into a config file. A
//! source the server does not carry is a TYPED ABSENCE here — never an empty
//! string handed to an agent that would then look logged out for a reason
//! nobody could see.
//!
//! # `cwd` is NOT a boundary
//!
//! An ACP agent's tools are not confined by the directory it was started in, and
//! the catalogue's launch lines carry no confinement flag of their own (they are
//! each agent's documented ACP invocation, verbatim, and `--workspace-root` is
//! not a flag `npx @agentclientprotocol/claude-agent-acp` accepts). What
//! confines the agent is the CONTAINMENT — it runs in its own process group and
//! the session takes the group with it — and what roots it is the server's own
//! working directory, which is the one directory an operator has already chosen
//! by starting the server there. Nothing here asks which directory the repo is
//! in; the console tells the agent through `assistant_context`.
//!
//! # Values never reach a log line
//!
//! The declaration is NAMES; the resolved values are placed on the `Command` and
//! never rendered. Nothing here logs an environment value, and nothing puts the
//! session bearer anywhere but into the MCP server specification.
//! `launch_tests.rs` pins both.
use PathBuf;
use Duration;
use AssistantSessionId;
use CatalogueHarness;
use ;
use ;
use crateResolvedAssistantAccount;
use AssistantSessionError;
use ;
/// The environment variable NAMES every assistant agent is launched with.
///
/// A fixed, stated set rather than an operator knob, because the round-2
/// amendment retired the knob and because every name here is needed for the
/// catalogue's own launch lines to work at all:
///
/// - `PATH` — `npx` and `opencode` are looked up on it; without it nothing execs.
/// - `HOME` — where the harness keeps its OWN login state (`~/.claude`,
/// `~/.codex`) and where `npx` caches the adapter it fetches. This is the
/// variable that makes "log the harness in on the server host" true.
/// - `TMPDIR` — `npx` unpacks into it; on macOS it is per-user and absent from a
/// cleared environment.
/// - `SHELL`, `USER`, `LOGNAME` — an agent that runs a tool runs it through a
/// shell, and the tools it runs (git, most of all) identify the user by these.
/// - `LANG`, `LC_ALL` — text encoding; an agent reading source under the C
/// locale mangles anything that is not ASCII.
/// - `TERM` — some agents refuse to start with no terminal type at all.
/// - `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_CACHE_HOME`, `XDG_STATE_HOME` —
/// where a harness keeps its login state on Linux when the operator moved it.
/// - `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` and their lowercase spellings — on
/// a corporate network an agent with no proxy cannot reach its own API, and
/// the failure surfaces as an inscrutable timeout.
/// - `SSL_CERT_FILE`, `SSL_CERT_DIR`, `NODE_EXTRA_CA_CERTS` — the same story for
/// a private certificate authority.
///
/// A name that is not here can still reach a particular agent: an account
/// declares it, which is what the `[assistant]` section is now FOR.
const AGENT_ENVIRONMENT: & = &;
/// How long the agent's exit is awaited after its stdin closes and its process
/// group has been asked to stop, before the group is killed.
///
/// NOT an operator knob and not a policy: it is the distribution's ONE
/// termination grace, imported by reference from the process-group containment
/// every declared body already runs under, so the assistant's agent and a
/// worker-owned command are stopped on the same ladder with the same window.
/// The round-2 amendment retired `exit_grace` as configuration; what is left is
/// the mechanism's own bound, and a second copy of that number is what would
/// make the two drift.
const TERMINATION_GRACE: Duration = PROCESS_GROUP_TERMINATION_GRACE;
/// How an assistant agent's permission requests are answered.
///
/// `allow-once` — approve one-shot requests, never a durable "remember this"
/// grant — and the reasoning is worth stating, because it is the one product
/// decision here that is not forced.
///
/// The operator summoned this agent from their own console, in their own
/// session, on their own machine, and every request and decision is recorded on
/// the transcript they are watching. `deny` would make a stock server's
/// assistant unable to read a file or run a check — the whole of what it is for
/// — and, with the knob retired, would leave the operator nothing to change. A
/// durable grant would outlive the conversation that gave it. So: one-shot,
/// recorded, and bounded by the process group the session takes with it.
///
/// An in-console permission prompt is a stated non-goal of this cut (contract
/// §3); this is the policy that stands in its place, and the surface says so.
const PERMISSION: PermissionPolicy = AllowOnce;
/// Where a spawned agent dials this server back.
///
/// TWO routes, one address. The general `/mcp` catalogue is the workflow tool
/// surface and is dark unless `[mcp] enabled`; `/assistant/mcp` is the
/// assistant's OWN catalogue and is served whenever this server can state an
/// address at all, because it is how a session's agent learns what is on the
/// operator's screen — the answer to "never ask which directory the repo is in".
///
/// `None` for the whole struct means this server cannot state a dialable address
/// (a configured port of zero, whose real port is only known after bind), and
/// then no MCP server of ours is handed over at all.
/// Everything one spawn needs.
pub
/// Build the harness one session runs on.
///
/// # Errors
///
/// [`AssistantSessionError::HarnessUnavailable`] when the catalogue entry's
/// program does not resolve on this server's `PATH` — re-measured HERE, at the
/// spawn, because availability is a fact about this machine now and not about
/// the machine as it was when the session was created.
/// [`AssistantSessionError::AccountEnvironmentAbsent`] when an account names a
/// server variable this server does not carry.
/// [`AssistantSessionError::Internal`] when the server has no working directory
/// to root the agent in, or the declaration cannot be built.
pub
/// The child's declared environment, and the account's variables resolved into
/// the names the child is given.
///
/// Two passes through the SAME [`EnvironmentDeclaration`] discipline, because
/// they answer two different questions. The stated set is an allow-list: a
/// server with no `TERM` is a server with no `TERM`, and the child simply does
/// not get one. An ACCOUNT's names are a REQUIREMENT: the operator declared them
/// to select a login, so a source this server does not carry is a typed absence
/// naming the variable — never an empty string, which would present to the
/// operator as a harness that is mysteriously logged out.
///
/// The account's value is then carried under the name the account said, which is
/// what lets two accounts select two different login directories through the one
/// variable a harness actually reads.
/// The directory an assistant agent is rooted at: the server's own.
///
/// The one directory an operator has already chosen — they started the server
/// there — and the only one available without asking a question the amendment
/// forbids asking. It is not a boundary (see the module docs) and is not
/// pretended to be one.
/// The MCP servers this session's agent is told about, and the bearer that
/// identifies it to both of this server's own routes.
///
/// TWO specs of ours, never one merged catalogue: the general `/mcp` tools are
/// the workflow surface any authorized caller reaches, and `assistant_context`
/// is about THIS conversation and belongs to nobody else. Publishing it in the
/// general catalogue would make one session's screen a tool every caller could
/// see listed; giving it its own route with its own catalogue and the session
/// bearer as the only credential is what keeps the two apart by construction
/// rather than by a filter somebody has to remember to apply.
///
/// ONE bearer covers both, because both are this server and the identity being
/// asserted — "I am session X's agent" — is the same on each.
///
/// The general route is handed over exactly when it is MOUNTED. It used to take
/// an `[assistant.tools] aion` switch as well; with that knob retired, `[mcp]
/// enabled` is the one place an operator says whether this server publishes
/// workflow tools at all, and a second switch saying it again is a second thing
/// to keep in step.
/// The headers that say WHICH session is calling, and prove it.
/// The name the agent shows for this server's own workflow tools.
pub const AION_MCP_SERVER_NAME: &str = "aion";
/// The name the agent shows for the assistant's own tools.
pub const ASSISTANT_MCP_SERVER_NAME: &str = "assistant";
/// The header the session bearer rides on.
const AUTHORIZATION_HEADER: &str = "authorization";