1use std::sync::Arc;
2
3use serde_json::Value;
4
5use super::budget::RunBudget;
6use crate::host::{CodeModeHost, ExecCtx};
7use crate::local_provider::{
8 dispatch_local_provider, parse_local_provider_call, LocalProviderCall,
9};
10use crate::types::{
11 CodeModeCaller, CodeModeExecutedCall, CodeModeSurface, ToolDescriptor, ToolScope, UiLink,
12};
13use crate::ToolError;
14
15pub(crate) struct ToolCallContext<'a, H: CodeModeHost> {
16 pub(crate) host: Option<&'a H>,
17 pub(crate) entries: &'a [ToolDescriptor],
18 pub(crate) caller: &'a CodeModeCaller,
19 pub(crate) surface: CodeModeSurface,
20 pub(crate) scope: &'a ToolScope,
21 pub(crate) execution_id: &'a Option<Arc<str>>,
22 pub(crate) ui_capture: &'a Arc<std::sync::Mutex<Option<UiLink>>>,
23 pub(crate) calls: &'a mut Vec<CodeModeExecutedCall>,
24}
25
26pub(crate) async fn handle_tool_call<H: CodeModeHost>(
27 ctx: &mut ToolCallContext<'_, H>,
28 budget: &mut RunBudget,
29 seq: u64,
30 id: String,
31 params: Value,
32) -> Result<Value, ToolError> {
33 budget.record_operation("tool call")?;
34 let result = if let Some(call) = parse_local_provider_call(&id, params.clone())? {
35 if !local_providers_allowed(ctx.caller, ctx.scope) {
36 Err(ToolError::Forbidden {
37 message: format!("Code Mode local provider `{id}` is not available in this scope"),
38 required_scopes: vec!["soma:admin".to_string()],
39 })
40 } else {
41 dispatch_scoped_local_provider(ctx.host, call).await
42 }
43 } else {
44 call_catalog_tool(ctx, seq, &id, params.clone()).await
45 };
46 let result = result.map(|value| budget.cap_tool_result(value));
47 match &result {
48 Ok(value) => ctx.calls.push(CodeModeExecutedCall {
49 id,
50 params: Some(params),
51 result: Some(value.clone()),
52 }),
53 Err(_) => ctx.calls.push(CodeModeExecutedCall {
54 id,
55 params: Some(params),
56 result: None,
57 }),
58 }
59 result
60}
61
62async fn call_catalog_tool<H: CodeModeHost>(
63 ctx: &mut ToolCallContext<'_, H>,
64 seq: u64,
65 id: &str,
66 params: Value,
67) -> Result<Value, ToolError> {
68 let host = ctx.host.ok_or_else(|| unknown_tool(id, ctx.entries))?;
69 let descriptor = ctx
70 .entries
71 .iter()
72 .find(|entry| entry.id == id)
73 .ok_or_else(|| unknown_tool(id, ctx.entries))?;
74 let outcome = call_host_tool_with_ctx(
75 host,
76 descriptor,
77 params,
78 ctx.caller,
79 ctx.surface,
80 ctx.scope,
81 ExecCtx {
82 seq,
83 execution_id: ctx.execution_id.clone(),
84 step_ordinal: None,
85 },
86 )
87 .await?;
88 if let Some(ui) = outcome.ui.clone() {
89 if let Ok(mut guard) = ctx.ui_capture.lock() {
90 *guard = Some(ui);
91 }
92 }
93 Ok(outcome.value)
94}
95
96async fn dispatch_scoped_local_provider<H: CodeModeHost>(
97 host: Option<&H>,
98 call: LocalProviderCall,
99) -> Result<Value, ToolError> {
100 #[cfg(feature = "openapi")]
101 if matches!(
102 call.provider,
103 crate::local_provider::LocalProviderName::Openapi
104 ) {
105 let host = host.ok_or_else(|| ToolError::UnknownInstance {
106 message: "OpenAPI provider requires a Code Mode host".to_string(),
107 valid: Vec::new(),
108 })?;
109 let registry = host
110 .openapi_registry()
111 .ok_or_else(|| ToolError::UnknownInstance {
112 message: "Code Mode host has no OpenAPI registry".to_string(),
113 valid: Vec::new(),
114 })?;
115 let client = host
116 .openapi_http_client()
117 .ok_or_else(|| ToolError::internal_message("Code Mode host has no OpenAPI client"))?;
118 return crate::openapi_feature::dispatch_openapi_provider(
119 ®istry,
120 &client,
121 &call.method,
122 call.params,
123 )
124 .await;
125 }
126 let _ = host;
127 dispatch_local_provider(call).await
128}
129
130async fn call_host_tool_with_ctx<H: CodeModeHost>(
131 host: &H,
132 descriptor: &ToolDescriptor,
133 params: Value,
134 caller: &CodeModeCaller,
135 surface: CodeModeSurface,
136 scope: &ToolScope,
137 ctx: ExecCtx,
138) -> Result<crate::host::ToolCallOutcome, ToolError> {
139 if !scope.allows(&descriptor.id) {
140 return Err(ToolError::Forbidden {
141 message: format!("Code Mode scope does not allow `{}`", descriptor.id),
142 required_scopes: vec![descriptor.namespace.clone()],
143 });
144 }
145 crate::schema::validate_code_mode_params_against_schema(¶ms, descriptor.schema.as_ref())?;
146 host.call_tool(&descriptor.id, params, caller, surface, scope, ctx)
147 .await
148}
149
150pub(crate) fn local_providers_allowed(caller: &CodeModeCaller, scope: &ToolScope) -> bool {
151 matches!(scope, ToolScope::All)
152 && (caller.capabilities.admin || caller.capabilities.trusted_local)
153}
154
155fn unknown_tool(id: &str, entries: &[ToolDescriptor]) -> ToolError {
156 ToolError::UnknownAction {
157 message: format!("unknown Code Mode tool `{id}`"),
158 valid: entries.iter().map(|entry| entry.id.clone()).collect(),
159 hint: Some(crate::broker::code_mode_unknown_tool_hint()),
160 }
161}