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}
22
23impl OperatorSessionBindingProvider {
24 pub fn new(
27 operator_sessions: Arc<Mutex<HashMap<SessionId, Arc<OperatorSessionEntry>>>>,
28 roles_to_sid: Arc<Mutex<HashMap<String, SessionId>>>,
29 ) -> Self {
30 Self {
31 operator_sessions,
32 roles_to_sid,
33 }
34 }
35
36 async fn bind_operator(
37 &self,
38 request: &BindRequest,
39 ) -> Result<BindOutcome, BindingProviderError> {
40 let target = request.binding_target.as_deref().ok_or_else(|| {
44 BindingProviderError::Provider(format!(
45 "agent '{}' uses {:?} but declares no logical binding target",
46 request.agent, request.backend
47 ))
48 })?;
49 let Some(sid) = self.roles_to_sid.lock().await.get(target).cloned() else {
56 return Ok(BindOutcome::Unbound {
57 agent: request.agent.clone(),
58 reason: format!("no Operator session owns binding target '{target}'"),
59 });
60 };
61 let Some(entry) = self.operator_sessions.lock().await.get(&sid).cloned() else {
62 return Ok(BindOutcome::Unbound {
63 agent: request.agent.clone(),
64 reason: format!(
65 "Operator session '{sid}' for binding target '{target}' disappeared"
66 ),
67 });
68 };
69 let Some(manifest) = entry.capability_manifest.as_ref() else {
70 return Ok(BindOutcome::Unbound {
71 agent: request.agent.clone(),
72 reason: format!(
73 "Operator session '{sid}' for binding target '{target}' supplied no capability_manifest"
74 ),
75 });
76 };
77 ManifestBindingProvider::new(manifest.clone())
82 .bind(std::slice::from_ref(request))
83 .await?
84 .pop()
85 .ok_or_else(|| {
86 BindingProviderError::Provider(format!(
87 "Operator provider '{}' returned no outcome for agent '{}'",
88 manifest.provider_id, request.agent
89 ))
90 })
91 }
92}
93
94#[async_trait]
95impl AgentBindingProvider for OperatorSessionBindingProvider {
96 async fn bind(
97 &self,
98 requests: &[BindRequest],
99 ) -> Result<Vec<BindOutcome>, BindingProviderError> {
100 let mut outcomes = Vec::with_capacity(requests.len());
101 for request in requests {
102 let outcome = match request.backend {
103 BindingBackend::WsOperator | BindingBackend::WsClaudeCode => {
104 self.bind_operator(request).await?
105 }
106 BindingBackend::AgentBlockInProcess => BindOutcome::Bound {
110 receipt: BindReceipt {
111 agent: request.agent.clone(),
112 request_digest: request.request_digest.clone(),
113 provider_id: "mse-agent-block-in-process".to_string(),
114 provider_revision: Some(env!("CARGO_PKG_VERSION").to_string()),
115 resolved_model: request.requested_model.clone(),
116 effective_tools: request.requested_tools.clone(),
117 launch_variant: None,
118 capability_snapshot_digest: None,
119 },
120 },
121 };
122 outcomes.push(outcome);
123 }
124 Ok(outcomes)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use mlua_swarm::{AgentProviderCapability, AgentProviderManifest, BindingDigest};
132
133 fn request() -> BindRequest {
134 BindRequest {
135 agent: "coder".to_string(),
136 request_digest: BindingDigest::sha256("request"),
137 backend: BindingBackend::WsOperator,
138 binding_target: Some("main-ai".to_string()),
139 requested_model: Some("sonnet".to_string()),
140 requested_tools: vec!["Read".to_string()],
141 launch_variant: Some("mse-coder".to_string()),
142 }
143 }
144
145 async fn provider(manifest: Option<AgentProviderManifest>) -> OperatorSessionBindingProvider {
146 let sid = SessionId::new();
147 let entry = Arc::new(OperatorSessionEntry {
148 sid: sid.clone(),
149 token: "token".to_string(),
150 roles: vec!["main-ai".to_string()],
151 capability_manifest: manifest,
152 ws_session: Mutex::new(None),
153 });
154 let sessions = Arc::new(Mutex::new(HashMap::from([(sid.clone(), entry)])));
155 let roles = Arc::new(Mutex::new(HashMap::from([("main-ai".to_string(), sid)])));
156 OperatorSessionBindingProvider::new(sessions, roles)
157 }
158
159 fn expect_bound(outcome: &BindOutcome) -> &mlua_swarm::BindReceipt {
160 match outcome {
161 BindOutcome::Bound { receipt } => receipt,
162 BindOutcome::Unbound { agent, reason } => {
163 panic!("expected Bound, got Unbound({agent}): {reason}")
164 }
165 }
166 }
167
168 #[tokio::test]
169 async fn operator_manifest_resolves_to_untrusted_receipt() {
170 let manifest = AgentProviderManifest {
171 provider_id: "main-ai-self-report".to_string(),
172 provider_revision: Some("1".to_string()),
173 capabilities: vec![AgentProviderCapability {
174 launch_variant: Some("mse-coder".to_string()),
175 resolved_model: Some("claude-sonnet-4".to_string()),
176 effective_tools: vec!["Read".to_string(), "Write".to_string()],
177 capability_snapshot_digest: Some(BindingDigest::sha256("manifest")),
178 }],
179 };
180 let outcomes = provider(Some(manifest))
181 .await
182 .bind(&[request()])
183 .await
184 .unwrap();
185 assert_eq!(outcomes.len(), 1);
186 let receipt = expect_bound(&outcomes[0]);
187 assert_eq!(receipt.provider_id, "main-ai-self-report");
188 assert_eq!(receipt.request_digest, request().request_digest);
189 assert_eq!(receipt.effective_tools, ["Read", "Write"]);
190 }
191
192 #[tokio::test]
193 async fn missing_manifest_reports_unbound() {
194 let outcomes = provider(None).await.bind(&[request()]).await.unwrap();
195 assert_eq!(outcomes.len(), 1);
196 match &outcomes[0] {
197 BindOutcome::Unbound { agent, reason } => {
198 assert_eq!(agent, "coder");
199 assert!(
200 reason.contains("supplied no capability_manifest"),
201 "reason: {reason}"
202 );
203 }
204 BindOutcome::Bound { .. } => panic!("expected Unbound when no manifest was submitted"),
205 }
206 }
207
208 #[tokio::test]
209 async fn missing_role_reports_unbound() {
210 let sessions = Arc::new(Mutex::new(HashMap::new()));
213 let roles = Arc::new(Mutex::new(HashMap::new()));
214 let provider = OperatorSessionBindingProvider::new(sessions, roles);
215 let outcomes = provider.bind(&[request()]).await.unwrap();
216 match &outcomes[0] {
217 BindOutcome::Unbound { agent, reason } => {
218 assert_eq!(agent, "coder");
219 assert!(
220 reason.contains("no Operator session owns"),
221 "reason: {reason}"
222 );
223 }
224 BindOutcome::Bound { .. } => panic!("expected Unbound when the role has not joined"),
225 }
226 }
227
228 #[tokio::test]
229 async fn in_process_backend_is_attested_by_server_registry() {
230 let mut request = request();
231 request.backend = BindingBackend::AgentBlockInProcess;
232 request.binding_target = None;
233 request.launch_variant = None;
234 let outcomes = provider(None).await.bind(&[request.clone()]).await.unwrap();
235 let receipt = expect_bound(&outcomes[0]);
236 assert_eq!(receipt.provider_id, "mse-agent-block-in-process");
237 assert_eq!(receipt.effective_tools, request.requested_tools);
238 }
239}