1#![allow(non_snake_case)]
4
5use super::*;
6
7use std::path::Path;
8
9fn agent_roster_storage() -> Result<String, String> {
10 let state = crate::state::APP_STATE
11 .get()
12 .ok_or("Application not initialized")?;
13 let storage = state
14 .config
15 .lock()
16 .map_err(|e| e.to_string())?
17 .storage_path
18 .clone();
19 Ok(storage)
20}
21
22pub fn agent_roster_list() -> Result<serde_json::Value, String> {
23 let storage = agent_roster_storage()?;
24 let roster = crate::agent_registry::load_roster(Path::new(&storage));
25 serde_json::to_value(roster).map_err(|e| e.to_string())
26}
27
28pub fn agent_roster_get(slug: String) -> Result<serde_json::Value, String> {
29 let storage = agent_roster_storage()?;
30 match crate::agent_registry::get_agent(Path::new(&storage), &slug) {
31 Some(a) => serde_json::to_value(a).map_err(|e| e.to_string()),
32 None => Ok(serde_json::Value::Null),
33 }
34}
35
36pub fn agent_roster_upsert(agent_json: String) -> Result<(), String> {
37 let storage = agent_roster_storage()?;
38 let agent: crate::agent_registry::AgentDefinition =
39 serde_json::from_str(&agent_json).map_err(|e| format!("invalid agent JSON: {e}"))?;
40 crate::agent_registry::upsert_agent(Path::new(&storage), agent)
41}
42
43pub fn agent_roster_remove(slug: String) -> Result<(), String> {
44 let storage = agent_roster_storage()?;
45 crate::agent_registry::remove_agent(Path::new(&storage), &slug)
46}
47
48pub fn agent_runtime_status(slug: String) -> Result<serde_json::Value, String> {
53 let storage = agent_roster_storage()?;
54 let agent = crate::agent_registry::get_agent(Path::new(&storage), &slug)
55 .ok_or_else(|| format!("unknown agent @{slug}"))?;
56 let active = crate::api::load_active_model_record_from_disk();
57 let (backend, configured_model, resident) = match &agent.backend {
58 crate::agent_registry::AgentBackendSpec::LocalEngine { model_id } => {
59 let resident = active.as_ref().is_some_and(|active| {
60 model_id
61 .as_deref()
62 .map_or(true, |configured| configured == active.model_id)
63 });
64 ("local", model_id.clone(), resident)
65 }
66 crate::agent_registry::AgentBackendSpec::RemoteMcp { model, .. } => {
67 ("remote_mcp", model.clone(), false)
68 }
69 };
70 Ok(serde_json::json!({
71 "slug": agent.slug,
72 "enabled": agent.enabled,
73 "backend": backend,
74 "configured_model_id": configured_model,
75 "resident": resident,
76 "active_model_id": active.as_ref().map(|record| record.model_id.clone()),
77 "lifecycle_state": crate::model_lifecycle::lifecycle_label(
78 crate::model_lifecycle::get_model_lifecycle_state()
79 ),
80 "last_decode_tokens_per_sec": crate::model_lifecycle::get_last_decode_tok_s(),
81 "last_decode_at_unix": crate::model_lifecycle::get_last_decode_tok_s_at_unix(),
82 }))
83}
84
85pub fn mcp_list_local_tools() -> Result<serde_json::Value, String> {
90 let tools = crate::mcp_tool_loop::mcp_list_local_tools()?;
91 serde_json::to_value(tools).map_err(|e| e.to_string())
92}
93
94pub fn mcp_call_tool_gated(
97 agent_slug: String,
98 tool_name: String,
99 arguments_json: String,
100 principal_permitted: bool,
101) -> Result<String, String> {
102 let storage = agent_roster_storage()?;
103 crate::mcp_tool_loop::mcp_call_tool_gated(
104 Path::new(&storage),
105 &agent_slug,
106 &tool_name,
107 &arguments_json,
108 principal_permitted,
109 )
110}
111
112pub fn agent_set_allowed_mcp_tools(slug: String, tools: Vec<String>) -> Result<(), String> {
114 let storage = agent_roster_storage()?;
115 crate::mcp_tool_loop::agent_set_allowed_mcp_tools(Path::new(&storage), &slug, tools)
116}
117
118pub fn mcp_ensure_safe_tool_allowlist(slug: String) -> Result<serde_json::Value, String> {
121 let storage = agent_roster_storage()?;
122 let tools = crate::mcp_tool_loop::ensure_safe_tool_allowlist(Path::new(&storage), &slug)?;
123 serde_json::to_value(tools).map_err(|e| e.to_string())
124}
125
126pub fn agent_roster_add_remote(
130 slug: String,
131 display_name: String,
132 transport_kind: String,
133 endpoint: String,
134 infer_tool: Option<String>,
135 model: Option<String>,
136 system_prompt: Option<String>,
137) -> Result<(), String> {
138 use crate::agent_registry::{AgentBackendSpec, McpTransport};
139 if slug.trim().is_empty() {
140 return Err("agent slug is required".to_string());
141 }
142 let transport = match transport_kind.to_lowercase().as_str() {
143 "tcp" => {
144 let (host, port) = endpoint
145 .rsplit_once(':')
146 .ok_or_else(|| "TCP endpoint must be host:port".to_string())?;
147 let port: u16 = port
148 .trim()
149 .parse()
150 .map_err(|_| "invalid TCP port".to_string())?;
151 McpTransport::Tcp {
152 host: host.trim().to_string(),
153 port,
154 }
155 }
156 "http" => McpTransport::Http {
157 url: endpoint.trim().to_string(),
158 credential_id: None,
159 },
160 "stdio" => {
161 let mut parts = endpoint.split_whitespace().map(|s| s.to_string());
162 let command = parts
163 .next()
164 .ok_or_else(|| "stdio endpoint needs a command".to_string())?;
165 McpTransport::Stdio {
166 command,
167 args: parts.collect(),
168 }
169 }
170 other => return Err(format!("unknown transport '{other}' (use tcp|http|stdio)")),
171 };
172 let backend = AgentBackendSpec::RemoteMcp {
173 endpoint: endpoint.trim().to_string(),
174 transport,
175 infer_tool: infer_tool.filter(|s| !s.trim().is_empty()),
176 model: model.filter(|s| !s.trim().is_empty()),
177 };
178 let mut agent = crate::agent_registry::AgentDefinition::new(
179 slug,
180 display_name,
181 "Remote agent reached over MCP.".to_string(),
182 backend,
183 system_prompt.unwrap_or_default(),
184 );
185 agent.enabled = true;
186 let storage = agent_roster_storage()?;
187 crate::agent_registry::upsert_agent(Path::new(&storage), agent)
188}
189
190#[cfg(not(target_arch = "wasm32"))]
193pub fn provider_credential_store(connection_id: String, bearer: String) -> Result<(), String> {
194 crate::provider_credentials::store_bearer_credential(&connection_id, &bearer)
195}
196
197#[cfg(not(target_arch = "wasm32"))]
199pub fn provider_credential_remove(connection_id: String) -> Result<(), String> {
200 crate::provider_credentials::remove_bearer_credential(&connection_id)
201}
202
203#[cfg(not(target_arch = "wasm32"))]
206pub fn agent_remote_connection_test(slug: String) -> Result<serde_json::Value, String> {
207 let storage = agent_roster_storage()?;
208 let agent = crate::agent_registry::get_agent(Path::new(&storage), &slug)
209 .ok_or_else(|| format!("no agent '{slug}' in roster"))?;
210 let transport = match &agent.backend {
211 crate::agent_registry::AgentBackendSpec::RemoteMcp { transport, .. } => transport,
212 crate::agent_registry::AgentBackendSpec::LocalEngine { .. } => {
213 return Err("only remote MCP agents have a connection to test".into());
214 }
215 };
216 let tool_count = crate::remote_mcp::remote_mcp_probe(transport)?;
217 Ok(serde_json::json!({
218 "ok": true,
219 "agent_slug": agent.slug,
220 "tool_count": tool_count,
221 }))
222}
223
224pub fn agent_backend_kind(slug: Option<String>) -> Result<String, String> {
226 let slug = match slug {
227 Some(s) if !s.is_empty() => s,
228 _ => return Ok("local".to_string()),
229 };
230 let storage = agent_roster_storage()?;
231 match crate::agent_registry::get_agent(Path::new(&storage), &slug) {
232 Some(a) => Ok(match a.backend {
233 crate::agent_registry::AgentBackendSpec::LocalEngine { .. } => "local".to_string(),
234 crate::agent_registry::AgentBackendSpec::RemoteMcp { .. } => "remote".to_string(),
235 }),
236 None => Ok("local".to_string()),
237 }
238}
239
240#[cfg(not(target_arch = "wasm32"))]
244pub fn run_remote_agent_turn(
245 session_id: String,
246 slug: String,
247 prompt: String,
248 per_turn_consent: bool,
249) -> Result<serde_json::Value, String> {
250 use crate::agent_registry::AgentBackendSpec;
251 let storage = agent_roster_storage()?;
252 let agent = crate::agent_registry::get_agent(Path::new(&storage), &slug)
253 .ok_or_else(|| format!("no agent '{slug}' in roster"))?;
254 if !agent.enabled {
255 return Ok(remote_turn_blocked(&format!(
256 "agent '{}' is disabled",
257 agent.display_name
258 )));
259 }
260 match agent.execution_policy.remote_consent {
261 crate::agent_registry::RemoteConsentPolicy::Never => {
262 return Ok(remote_turn_blocked("this agent's remote connection is disabled by its policy"));
263 }
264 crate::agent_registry::RemoteConsentPolicy::PerTurn if !per_turn_consent => {
265 return Ok(remote_turn_blocked("remote dispatch requires this turn's explicit confirmation"));
266 }
267 crate::agent_registry::RemoteConsentPolicy::PerTurn
268 | crate::agent_registry::RemoteConsentPolicy::Preapproved => {}
269 }
270 let (transport, infer_tool, model) = match &agent.backend {
271 AgentBackendSpec::RemoteMcp {
272 transport,
273 infer_tool,
274 model,
275 ..
276 } => (transport.clone(), infer_tool.clone(), model.clone()),
277 AgentBackendSpec::LocalEngine { .. } => {
278 return Err("agent is local — use the local inference path".to_string());
279 }
280 };
281
282 let local_active = crate::model_lifecycle::lifecycle_label(
285 crate::model_lifecycle::get_model_lifecycle_state(),
286 ) == "Active";
287 let inputs = crate::job_router::RoutingInputs {
288 sensitivity: wellfare_core::record::SensitivityClass::Restricted,
289 local_available: local_active,
290 external_consented: per_turn_consent || matches!(
291 agent.execution_policy.remote_consent,
292 crate::agent_registry::RemoteConsentPolicy::Preapproved
293 ),
294 requires_capability: None,
295 local_has_capability: false,
296 estimated_cost_microcents: 0,
297 };
298 match crate::job_router::route_job(&inputs, &crate::job_router::RoutingPolicy::default()) {
299 crate::job_router::RoutingDecision::Blocked { reason }
300 | crate::job_router::RoutingDecision::NeedsConsent { reason } => {
301 return Ok(remote_turn_blocked(&reason));
302 }
303 _ => {}
304 }
305
306 let system = if agent.system_prompt.trim().is_empty() {
307 None
308 } else {
309 Some(agent.system_prompt.as_str())
310 };
311 let text = crate::remote_mcp::remote_mcp_infer(
312 &transport,
313 infer_tool.as_deref(),
314 model.as_deref(),
315 system,
316 &prompt,
317 )?;
318 if !text.trim().is_empty() {
319 let _ = append_chat_message(session_id, "agent".to_string(), text.clone());
320 }
321 Ok(serde_json::json!({
322 "text": text,
323 "committed": true,
324 "block_reason": serde_json::Value::Null,
325 "agent_backend": "remote",
326 "model_id": model,
327 "provenance_hashes": [],
328 "citations": [],
329 "tokens_generated": 0,
330 "inference_duration_ms": 0,
331 }))
332}
333
334#[cfg(not(target_arch = "wasm32"))]
335fn remote_turn_blocked(reason: &str) -> serde_json::Value {
336 serde_json::json!({
337 "text": "",
338 "committed": false,
339 "block_reason": reason,
340 "agent_backend": "remote",
341 })
342}
343
344pub fn ingest_chat_cml(session_id: String, text: String) -> Result<usize, String> {
347 let storage = agent_roster_storage()?;
348 crate::cml_context::ingest_turn(Path::new(&storage), &session_id, &text).map(|v| v.len())
349}