1use crate::operator_ws::login::OperatorSessionEntry;
8use async_trait::async_trait;
9use mlua_swarm::{
10 AgentBindingProvider, BindOutcome, BindReceipt, BindRequest, BindingBackend,
11 BindingProviderError, ManifestBindingProvider, SessionId,
12};
13use std::collections::HashMap;
14use std::sync::Arc;
15use tokio::sync::Mutex;
16
17pub struct OperatorSessionBindingProvider {
19 operator_sessions: Arc<Mutex<HashMap<SessionId, Arc<OperatorSessionEntry>>>>,
20 roles_to_sid: Arc<Mutex<HashMap<String, SessionId>>>,
21 pinned_sid: Option<SessionId>,
26}
27
28impl OperatorSessionBindingProvider {
29 pub fn new(
32 operator_sessions: Arc<Mutex<HashMap<SessionId, Arc<OperatorSessionEntry>>>>,
33 roles_to_sid: Arc<Mutex<HashMap<String, SessionId>>>,
34 ) -> Self {
35 Self {
36 operator_sessions,
37 roles_to_sid,
38 pinned_sid: None,
39 }
40 }
41
42 async fn resolve_sid(&self, target: &str) -> Result<SessionId, String> {
51 match &self.pinned_sid {
52 Some(sid) => Ok(sid.clone()),
53 None => self
54 .roles_to_sid
55 .lock()
56 .await
57 .get(target)
58 .cloned()
59 .ok_or_else(|| format!("no Operator session owns binding target '{target}'")),
60 }
61 }
62
63 async fn bind_operator(
64 &self,
65 request: &BindRequest,
66 ) -> Result<BindOutcome, BindingProviderError> {
67 let target = request.binding_target.as_deref().ok_or_else(|| {
71 BindingProviderError::Provider(format!(
72 "agent '{}' uses {:?} but declares no logical binding target",
73 request.agent, request.backend
74 ))
75 })?;
76 let sid = match self.resolve_sid(target).await {
83 Ok(sid) => sid,
84 Err(reason) => {
85 return Ok(BindOutcome::Unbound {
86 agent: request.agent.clone(),
87 reason,
88 });
89 }
90 };
91 let via = match &self.pinned_sid {
96 Some(_) => format!("run-scoped pin (declared binding target '{target}')"),
97 None => format!("binding target '{target}'"),
98 };
99 let Some(entry) = self.operator_sessions.lock().await.get(&sid).cloned() else {
100 return Ok(BindOutcome::Unbound {
101 agent: request.agent.clone(),
102 reason: format!("Operator session '{sid}' for {via} disappeared"),
103 });
104 };
105 let Some(manifest) = entry.capability_manifest.as_ref() else {
106 return Ok(BindOutcome::Unbound {
107 agent: request.agent.clone(),
108 reason: format!(
109 "Operator session '{sid}' for {via} supplied no capability_manifest"
110 ),
111 });
112 };
113 ManifestBindingProvider::new(manifest.clone())
118 .bind(std::slice::from_ref(request))
119 .await?
120 .pop()
121 .ok_or_else(|| {
122 BindingProviderError::Provider(format!(
123 "Operator provider '{}' returned no outcome for agent '{}'",
124 manifest.provider_id, request.agent
125 ))
126 })
127 }
128}
129
130#[async_trait]
131impl AgentBindingProvider for OperatorSessionBindingProvider {
132 async fn bind(
133 &self,
134 requests: &[BindRequest],
135 ) -> Result<Vec<BindOutcome>, BindingProviderError> {
136 let mut outcomes = Vec::with_capacity(requests.len());
137 for request in requests {
138 let outcome = match request.backend {
139 BindingBackend::WsOperator | BindingBackend::WsClaudeCode => {
140 self.bind_operator(request).await?
141 }
142 BindingBackend::AgentBlockInProcess => BindOutcome::Bound {
146 receipt: BindReceipt {
147 agent: request.agent.clone(),
148 request_digest: request.request_digest.clone(),
149 provider_id: "mse-agent-block-in-process".to_string(),
150 provider_revision: Some(env!("CARGO_PKG_VERSION").to_string()),
151 resolved_model: request.requested_model.clone(),
152 effective_tools: request.requested_tools.clone(),
153 launch_variant: None,
154 capability_snapshot_digest: None,
155 },
156 },
157 };
158 outcomes.push(outcome);
159 }
160 Ok(outcomes)
161 }
162
163 fn pinned_to_session(&self, session_id: &str) -> Option<Arc<dyn AgentBindingProvider>> {
169 let sid = SessionId::parse(session_id.to_string()).ok()?;
170 Some(Arc::new(Self {
171 operator_sessions: self.operator_sessions.clone(),
172 roles_to_sid: self.roles_to_sid.clone(),
173 pinned_sid: Some(sid),
174 }))
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use mlua_swarm::{AgentProviderCapability, AgentProviderManifest, BindingDigest};
182
183 fn request() -> BindRequest {
184 BindRequest {
185 agent: "coder".to_string(),
186 request_digest: BindingDigest::sha256("request"),
187 backend: BindingBackend::WsOperator,
188 binding_target: Some("main-ai".to_string()),
189 requested_model: Some("sonnet".to_string()),
190 requested_tools: vec!["Read".to_string()],
191 launch_variant: Some("mse-coder".to_string()),
192 }
193 }
194
195 async fn provider(manifest: Option<AgentProviderManifest>) -> OperatorSessionBindingProvider {
196 let sid = SessionId::new();
197 let entry = Arc::new(OperatorSessionEntry {
198 sid: sid.clone(),
199 token: "token".to_string(),
200 roles: vec!["main-ai".to_string()],
201 capability_manifest: manifest,
202 joined_at_secs: 0,
203 ws_session: Mutex::new(None),
204 });
205 let sessions = Arc::new(Mutex::new(HashMap::from([(sid.clone(), entry)])));
206 let roles = Arc::new(Mutex::new(HashMap::from([("main-ai".to_string(), sid)])));
207 OperatorSessionBindingProvider::new(sessions, roles)
208 }
209
210 fn expect_bound(outcome: &BindOutcome) -> &mlua_swarm::BindReceipt {
211 match outcome {
212 BindOutcome::Bound { receipt } => receipt,
213 BindOutcome::Unbound { agent, reason } => {
214 panic!("expected Bound, got Unbound({agent}): {reason}")
215 }
216 }
217 }
218
219 #[tokio::test]
220 async fn operator_manifest_resolves_to_untrusted_receipt() {
221 let manifest = AgentProviderManifest {
222 provider_id: "main-ai-self-report".to_string(),
223 provider_revision: Some("1".to_string()),
224 capabilities: vec![AgentProviderCapability {
225 launch_variant: Some("mse-coder".to_string()),
226 resolved_model: Some("claude-sonnet-4".to_string()),
227 effective_tools: vec!["Read".to_string(), "Write".to_string()],
228 capability_snapshot_digest: Some(BindingDigest::sha256("manifest")),
229 }],
230 };
231 let outcomes = provider(Some(manifest))
232 .await
233 .bind(&[request()])
234 .await
235 .unwrap();
236 assert_eq!(outcomes.len(), 1);
237 let receipt = expect_bound(&outcomes[0]);
238 assert_eq!(receipt.provider_id, "main-ai-self-report");
239 assert_eq!(receipt.request_digest, request().request_digest);
240 assert_eq!(receipt.effective_tools, ["Read", "Write"]);
241 }
242
243 #[tokio::test]
244 async fn missing_manifest_reports_unbound() {
245 let outcomes = provider(None).await.bind(&[request()]).await.unwrap();
246 assert_eq!(outcomes.len(), 1);
247 match &outcomes[0] {
248 BindOutcome::Unbound { agent, reason } => {
249 assert_eq!(agent, "coder");
250 assert!(
251 reason.contains("supplied no capability_manifest"),
252 "reason: {reason}"
253 );
254 }
255 BindOutcome::Bound { .. } => panic!("expected Unbound when no manifest was submitted"),
256 }
257 }
258
259 #[tokio::test]
260 async fn missing_role_reports_unbound() {
261 let sessions = Arc::new(Mutex::new(HashMap::new()));
264 let roles = Arc::new(Mutex::new(HashMap::new()));
265 let provider = OperatorSessionBindingProvider::new(sessions, roles);
266 let outcomes = provider.bind(&[request()]).await.unwrap();
267 match &outcomes[0] {
268 BindOutcome::Unbound { agent, reason } => {
269 assert_eq!(agent, "coder");
270 assert!(
271 reason.contains("no Operator session owns"),
272 "reason: {reason}"
273 );
274 }
275 BindOutcome::Bound { .. } => panic!("expected Unbound when the role has not joined"),
276 }
277 }
278
279 #[tokio::test]
285 async fn pinned_provider_attests_through_the_pin_not_the_role_holder() {
286 let manifest = AgentProviderManifest {
287 provider_id: "pinned-session".to_string(),
288 provider_revision: None,
289 capabilities: vec![AgentProviderCapability {
290 launch_variant: Some("mse-coder".to_string()),
291 resolved_model: Some("claude-sonnet-4".to_string()),
292 effective_tools: vec!["Read".to_string()],
293 capability_snapshot_digest: None,
294 }],
295 };
296 let role_holder_sid = SessionId::new();
297 let pinned_sid = SessionId::new();
298 let role_holder = Arc::new(OperatorSessionEntry {
299 sid: role_holder_sid.clone(),
300 token: "token".to_string(),
301 roles: vec!["main-ai".to_string()],
302 capability_manifest: None,
303 joined_at_secs: 0,
304 ws_session: Mutex::new(None),
305 });
306 let pinned = Arc::new(OperatorSessionEntry {
307 sid: pinned_sid.clone(),
308 token: "token".to_string(),
309 roles: Vec::new(),
310 capability_manifest: Some(manifest),
311 joined_at_secs: 0,
312 ws_session: Mutex::new(None),
313 });
314 let sessions = Arc::new(Mutex::new(HashMap::from([
315 (role_holder_sid.clone(), role_holder),
316 (pinned_sid.clone(), pinned),
317 ])));
318 let roles = Arc::new(Mutex::new(HashMap::from([(
319 "main-ai".to_string(),
320 role_holder_sid,
321 )])));
322 let provider = OperatorSessionBindingProvider::new(sessions, roles);
323
324 let outcomes = provider.bind(&[request()]).await.unwrap();
326 assert!(
327 matches!(&outcomes[0], BindOutcome::Unbound { .. }),
328 "the role's holder supplied no manifest, so the unpinned bind is Unbound"
329 );
330
331 let pinned_provider = provider
334 .pinned_to_session(pinned_sid.as_str())
335 .expect("a live sid must yield a pinned provider");
336 let outcomes = pinned_provider.bind(&[request()]).await.unwrap();
337 let receipt = expect_bound(&outcomes[0]);
338 assert_eq!(receipt.provider_id, "pinned-session");
339 assert_eq!(receipt.effective_tools, ["Read"]);
340 }
341
342 #[tokio::test]
346 async fn pin_to_a_dead_session_reports_unbound_naming_the_pin() {
347 let provider = provider(None).await;
348 let gone = SessionId::new();
349 let pinned = provider
350 .pinned_to_session(gone.as_str())
351 .expect("pinned provider");
352 let outcomes = pinned.bind(&[request()]).await.unwrap();
353 match &outcomes[0] {
354 BindOutcome::Unbound { reason, .. } => {
355 assert!(
356 reason.contains("run-scoped pin"),
357 "reason must attribute the gap to the pin: {reason}"
358 );
359 assert!(
360 reason.contains(gone.as_str()),
361 "reason must name the pinned sid: {reason}"
362 );
363 }
364 BindOutcome::Bound { .. } => panic!("a pin to a dead session cannot be Bound"),
365 }
366 }
367
368 #[test]
372 fn unparseable_pin_yields_no_pinned_provider() {
373 let sessions = Arc::new(Mutex::new(HashMap::new()));
374 let roles = Arc::new(Mutex::new(HashMap::new()));
375 let provider = OperatorSessionBindingProvider::new(sessions, roles);
376 assert!(provider.pinned_to_session("not-a-session-id").is_none());
377 }
378
379 #[tokio::test]
380 async fn in_process_backend_is_attested_by_server_registry() {
381 let mut request = request();
382 request.backend = BindingBackend::AgentBlockInProcess;
383 request.binding_target = None;
384 request.launch_variant = None;
385 let outcomes = provider(None).await.bind(&[request.clone()]).await.unwrap();
386 let receipt = expect_bound(&outcomes[0]);
387 assert_eq!(receipt.provider_id, "mse-agent-block-in-process");
388 assert_eq!(receipt.effective_tools, request.requested_tools);
389 }
390}