Skip to main content

soma_codemode/execute/
tool_dispatch.rs

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            &registry,
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(&params, 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}