mod models;
mod request;
mod response;
#[cfg(test)]
mod tests;
use actix_web::{web, HttpResponse};
use bamboo_agent_core::tools::{ToolExecutionContext, ToolExecutionSessionFlags};
use crate::app_state::AppState;
use crate::error::AppError;
pub use models::{
ToolExecutionRequest, ToolExecutionResponse, ToolExecutionResultPayload, ToolParameter,
};
fn enforce_plan_tool_gate(
session_flags: ToolExecutionSessionFlags,
tool_name: &str,
) -> Result<(), AppError> {
if session_flags.plan_read_only && !bamboo_tools::orchestrator::plan_mode_allows_tool(tool_name)
{
return Err(AppError::ToolExecutionError(format!(
"Plan mode: {tool_name} operation blocked"
)));
}
Ok(())
}
fn direct_tool_session_flags(
configured_mode: bamboo_domain::PermissionMode,
session: Option<&bamboo_agent_core::Session>,
) -> ToolExecutionSessionFlags {
if let Some(session) = session {
return ToolExecutionSessionFlags::from_session_and_configured_mode(
session,
configured_mode,
);
}
let resolution = bamboo_domain::resolve_permission_mode(
bamboo_domain::SessionPermissionMode::Default,
configured_mode,
);
ToolExecutionSessionFlags {
bypass_permissions: resolution.bypass_permissions(),
auto_approve_permissions: resolution.suppress_approval_prompts(),
plan_read_only: resolution.effective == bamboo_domain::PermissionMode::Plan,
}
}
pub async fn execute_tool(
app_state: web::Data<AppState>,
payload: web::Json<ToolExecutionRequest>,
) -> Result<HttpResponse, AppError> {
let ToolExecutionRequest {
tool_name,
parameters,
session_id,
} = payload.into_inner();
let canonical_tool_name = request::canonical_tool_name_or_error(&tool_name)?;
let session_id = request::trimmed_session_id(session_id.as_deref());
request::validate_session_context_requirement(&canonical_tool_name, session_id)?;
let args = request::parse_arguments(parameters);
let call = request::build_tool_call(canonical_tool_name, args)?;
let configured_mode = app_state
.permission_checker
.permission_config()
.map(|config| config.mode())
.unwrap_or_default();
let persisted_session = if let Some(session_id) = session_id {
app_state.session_repo.try_load(session_id).await?
} else {
None
};
let session_flags = direct_tool_session_flags(configured_mode, persisted_session.as_ref());
enforce_plan_tool_gate(session_flags, &call.function.name)?;
let root_tools = app_state.tools_for(crate::tools::ToolSurface::Root);
let available_tool_schemas = root_tools.list_tools();
let result = root_tools
.execute_with_context(
&call,
ToolExecutionContext {
session_id,
tool_call_id: &call.id,
event_tx: None,
available_tool_schemas: Some(available_tool_schemas.as_slice()),
bypass_permissions: session_flags.bypass_permissions,
auto_approve_permissions: session_flags.auto_approve_permissions,
plan_read_only: session_flags.plan_read_only,
can_async_resume: false,
bash_completion_sink: None,
pre_parsed_args: None,
},
)
.await
.map_err(|err| AppError::ToolExecutionError(err.to_string()))?;
let response = response::build_execution_response(tool_name, result)?;
Ok(HttpResponse::Ok().json(response))
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route("/tools/execute", web::post().to(execute_tool));
}