basis_acp/server/config.rs
1//! Where a connection's sessions come from, and what its client cannot say.
2//!
3//! A [`ServeConfig`] is a [`SessionSource`] plus the mode its sessions open
4//! in, and the source [`ServeConfig::new`] reaches for is
5//! [`ConfiguredSource`](super::workspaces::ConfiguredSource) — which is next
6//! door rather than here, because since ADR-0018 it is no longer a mapping
7//! from a template to a config. It holds the process's runtime and a workspace
8//! per directory, which is a lifetime rather than a configuration, and
9//! configuration is all this file is.
10//!
11//! Nothing here answers a request. The handlers read this and never write it,
12//! which is why it is `Clone` and holds no lock: every closure in
13//! [`serve`](super::serve) gets its own copy.
14
15use std::{path::PathBuf, sync::Arc};
16
17use super::workspaces::ConfiguredSource;
18use crate::mode::ApprovalMode;
19use basis::{McpServer, PersistedSession, PreparedRun, RunConfig, RunError};
20
21/// Where an ACP session's [`PreparedRun`] comes from.
22///
23/// The same seam as [`prepare_with_session`](basis::run::prepare_with_session),
24/// at the protocol layer: a Rust host that already owns a mentra runtime —
25/// custom tools, its own store, a provider basis does not know — can serve ACP
26/// over it instead of letting basis build one. basis's own tests are the other
27/// consumer, driving the whole server against a scripted runtime with no
28/// network.
29///
30/// A source that builds its own runtime owns its tool authorizer too, and a
31/// session mode only reaches calls that authorizer surfaces: install
32/// [`ApprovalGate`](basis::approval::ApprovalGate) — which is what basis's own
33/// source gets from the [`Runtime`](basis::Runtime) it builds — or the
34/// client's mode picker will have nothing to decide.
35#[async_trait::async_trait]
36pub trait SessionSource: Send + Sync + 'static {
37 /// Opens a conversation in `cwd`, for `session/new`, with the MCP servers
38 /// the client configured for this session.
39 async fn create(&self, cwd: PathBuf, mcp: Vec<McpServer>) -> Result<PreparedRun, RunError>;
40
41 /// Picks up the conversation persisted under `agent_id`, for
42 /// `session/load`. The default refuses, which is the honest answer for a
43 /// source whose sessions do not outlive the process.
44 async fn resume(
45 &self,
46 agent_id: &str,
47 cwd: PathBuf,
48 mcp: Vec<McpServer>,
49 ) -> Result<PreparedRun, RunError> {
50 let _ = (agent_id, cwd, mcp);
51 Err(RunError::NoSuchSession)
52 }
53
54 /// Whether this source can enumerate the conversations it has persisted.
55 ///
56 /// `session/list` is advertised and answered only when this is true. A
57 /// source that keeps no registry would otherwise report "no sessions" for
58 /// a workspace that has some, and a capability that answers wrongly is
59 /// worse than one that was never claimed — an unregistered method at least
60 /// says so, with `-32601`.
61 fn lists_sessions(&self) -> bool {
62 false
63 }
64
65 /// Every conversation persisted for `cwd`, oldest first.
66 ///
67 /// Only called when [`lists_sessions`](Self::lists_sessions) is true, so
68 /// the default is unreachable rather than a claim about anything.
69 async fn list_sessions(&self, cwd: PathBuf) -> Result<Vec<PersistedSession>, RunError> {
70 let _ = cwd;
71 Ok(Vec::new())
72 }
73}
74
75/// How a served connection is configured.
76///
77/// The client supplies the workspace per session (`cwd` on `session/new`), so
78/// what belongs here is only what the client cannot say: which model and
79/// endpoint to use, whether commands are granted, and which permission mode
80/// each session opens in.
81///
82/// One of these describes a *server*, not a connection: it is cloned into every
83/// handler and, on the bridge, into every connection served, so the runtime and
84/// workspaces its source holds are the process's (ADR-0018). Building a second
85/// one builds a second runtime.
86#[derive(Clone)]
87pub struct ServeConfig {
88 pub(super) source: Arc<dyn SessionSource>,
89 /// Where a new session's mode picker starts.
90 pub(super) initial_mode: ApprovalMode,
91}
92
93impl std::fmt::Debug for ServeConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("ServeConfig")
96 .field("initial_mode", &self.initial_mode)
97 .finish_non_exhaustive()
98 }
99}
100
101impl Default for ServeConfig {
102 fn default() -> Self {
103 Self::new(None)
104 }
105}
106
107impl ServeConfig {
108 /// Serves sessions built from `template`, whose workspace each session
109 /// replaces with the `cwd` its client sent.
110 ///
111 /// The template's process half — provider, endpoint, model — becomes the
112 /// recipe for the one [`Runtime`](basis::Runtime) every session runs on
113 /// (ADR-0018), built on the first `session/new` rather than here, so that a
114 /// missing credential still reaches the client as `auth_required` rather
115 /// than stopping the server from starting.
116 ///
117 /// Sessions open at [`ApprovalMode::Prompt`] rather than at basis's library
118 /// default of allowing everything: over ACP there is a client to ask, which
119 /// is the whole reason the protocol carries a permission request. An
120 /// operator who wants otherwise says so with
121 /// [`with_initial_mode`](Self::with_initial_mode) — the template cannot
122 /// carry it, because a [`RunConfig`] no longer has an opinion about
123 /// approval to carry (ADR-0010).
124 pub fn new(template: impl Into<Option<RunConfig>>) -> Self {
125 Self {
126 source: Arc::new(ConfiguredSource::new(template.into())),
127 initial_mode: ApprovalMode::default(),
128 }
129 }
130
131 /// Serves sessions the caller supplies.
132 pub fn with_source(source: impl SessionSource) -> Self {
133 Self {
134 source: Arc::new(source),
135 initial_mode: ApprovalMode::default(),
136 }
137 }
138
139 /// Opens each session in `mode` instead of asking every time.
140 pub fn with_initial_mode(self, mode: ApprovalMode) -> Self {
141 Self {
142 initial_mode: mode,
143 ..self
144 }
145 }
146}