Fix frontend extension session state and tool routes (#8464)
Signed-off-by: sunilkumarvalmiki <g.sunilkumarvalmiki@gmail.com> Signed-off-by: jh-block <jhugo@block.xyz> Signed-off-by: Douwe Osinga <douwe@squareup.com> Signed-off-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Angie Jones <jones.angie@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com> Signed-off-by: Adam Miller <admiller@redhat.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com> Signed-off-by: Andrew Harvard <aharvard@squareup.com> Signed-off-by: Matt Toohey <contact@matttoohey.com> Signed-off-by: Wes <wesb@block.xyz> Signed-off-by: Clay Delk <clay.delk@gmail.com> Signed-off-by: Kalvin Chau <kalvin@block.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Alex Hancock <alexhancock@block.xyz> Signed-off-by: Treebird <treebird@treebird.dev> Signed-off-by: tulsi <tulsi@block.xyz> Signed-off-by: vonbai <nswanqi@gmail.com> Signed-off-by: olaservo <olahungerford@gmail.com> Signed-off-by: Abhijay Jain <Abhijay007j@gmail.com> Signed-off-by: Rodolfo Olivieri <rolivier@redhat.com> Signed-off-by: fresh3nough <anonwurcod@proton.me> Signed-off-by: Jheison Martinez Bolivar <jheison.mb@gmail.com> Co-authored-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Lifei Zhou <lifei@squareup.com> Co-authored-by: jh-block <jhugo@block.xyz> Co-authored-by: Alex Hancock <alexhancock@block.xyz> Co-authored-by: Jack Amadeo <jackamadeo@block.xyz> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Angie Jones <jones.angie@gmail.com> Co-authored-by: SomeSolutionsArchitect <139817767+SomeSolutionsArchitect@users.noreply.github.com> Co-authored-by: Christian <chvargas@wfscorp.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com> Co-authored-by: Adam Miller <admiller@redhat.com> Co-authored-by: Jack Amadeo <jackamadeo@squareup.com> Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Andrew Harvard <aharvard@squareup.com> Co-authored-by: Kalvin C <kalvinnchau@users.noreply.github.com> Co-authored-by: Adewale Abati <acekyd01@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Kalvin Chau <kalvin@block.xyz> Co-authored-by: Clay Delk <clay.delk@gmail.com> Co-authored-by: dorien-koelemeijer <62866702+dorien-koelemeijer@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Monroe Williams <monroe@pobox.com> Co-authored-by: Spike Wang <spike@spikewang.me> Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Treebird <treebird@treebird.dev> Co-authored-by: Sarthak Bhardwaj <100398847+SarthakB11@users.noreply.github.com> Co-authored-by: Douwe Osinga <douwe@block.xyz> Co-authored-by: tulsi <tulsi@block.xyz> Co-authored-by: Vonbai <107612985+vonbai@users.noreply.github.com> Co-authored-by: Ola Hungerford <olahungerford@gmail.com> Co-authored-by: Lucas Kim <45152028+Raptors65@users.noreply.github.com> Co-authored-by: Abhijay Jain <abhijay007j@gmail.com> Co-authored-by: Rodolfo Olivieri <rolivier@redhat.com> Co-authored-by: fre$h <anonwurcod@proton.me> Co-authored-by: Jheison Martinez Bolivar <78370841+JheisonMB@users.noreply.github.com>
This commit is contained in:
@@ -1054,16 +1054,17 @@ async fn read_resource(
|
||||
request_body = CallToolRequest,
|
||||
responses(
|
||||
(status = 200, description = "Resource read successfully", body = CallToolResponse),
|
||||
(status = 403, description = "Forbidden - tool is not app-visible", body = ErrorResponse),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
(status = 404, description = "Resource not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 424, description = "Frontend tool execution requires the frontend host", body = ErrorResponse),
|
||||
(status = 404, description = "Resource not found", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
async fn call_tool(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<CallToolRequest>,
|
||||
) -> Result<Json<CallToolResponse>, StatusCode> {
|
||||
) -> Result<Json<CallToolResponse>, ErrorResponse> {
|
||||
ensure_extensions_loaded(&state, &payload.session_id).await;
|
||||
|
||||
let agent = state
|
||||
@@ -1078,10 +1079,23 @@ async fn call_tool(
|
||||
tool = %payload.name,
|
||||
"Rejected app call to model-only tool"
|
||||
);
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
return Err(ErrorResponse {
|
||||
message: format!("Tool '{}' cannot be called by the app", payload.name),
|
||||
status: StatusCode::FORBIDDEN,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if agent.is_frontend_tool(&payload.name).await {
|
||||
return Err(ErrorResponse {
|
||||
message: format!(
|
||||
"Tool '{}' is provided by the frontend and must be executed by the frontend host",
|
||||
payload.name
|
||||
),
|
||||
status: StatusCode::FAILED_DEPENDENCY,
|
||||
});
|
||||
}
|
||||
|
||||
let arguments = match payload.arguments {
|
||||
Value::Object(map) => Some(map),
|
||||
_ => None,
|
||||
@@ -1100,19 +1114,19 @@ async fn call_tool(
|
||||
.extension_manager
|
||||
.dispatch_tool_call(&ctx, tool_call, CancellationToken::default())
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
.map_err(ErrorResponse::from)?;
|
||||
|
||||
let result = tool_result
|
||||
.result
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let result = tool_result.result.await.map_err(|err| ErrorResponse {
|
||||
message: err.to_string(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
let content = result
|
||||
.content
|
||||
.into_iter()
|
||||
.map(serde_json::to_value)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
.map_err(ErrorResponse::from)?;
|
||||
|
||||
Ok(Json(CallToolResponse {
|
||||
content,
|
||||
@@ -1340,3 +1354,87 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/agent/stop", post(stop_agent))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use goose::config::GooseMode;
|
||||
use goose::session::session_manager::SessionType;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::object;
|
||||
|
||||
fn frontend_extension() -> ExtensionConfig {
|
||||
ExtensionConfig::Frontend {
|
||||
name: "frontend-e2e".to_string(),
|
||||
description: "Frontend test extension".to_string(),
|
||||
tools: vec![Tool::new(
|
||||
"frontend__echo".to_string(),
|
||||
"Echo a string from the frontend".to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": { "type": "string" }
|
||||
},
|
||||
"required": ["message"]
|
||||
}),
|
||||
)],
|
||||
instructions: Some("Use the frontend echo tool.".to_string()),
|
||||
bundled: None,
|
||||
available_tools: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn frontend_extensions_are_listed_and_rejected_cleanly_by_call_tool() {
|
||||
let state = AppState::new(true).await.unwrap();
|
||||
let session = state
|
||||
.session_manager()
|
||||
.create_session(
|
||||
std::env::current_dir().unwrap(),
|
||||
"frontend-route-test".to_string(),
|
||||
SessionType::Hidden,
|
||||
GooseMode::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
agent_add_extension(
|
||||
State(state.clone()),
|
||||
Json(AddExtensionRequest {
|
||||
session_id: session.id.clone(),
|
||||
config: frontend_extension(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let Json(tools) = get_tools(
|
||||
State(state.clone()),
|
||||
Query(GetToolsQuery {
|
||||
extension_name: None,
|
||||
session_id: session.id.clone(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(tools.iter().any(|tool| tool.name == "frontend__echo"));
|
||||
|
||||
let error = match call_tool(
|
||||
State(state),
|
||||
Json(CallToolRequest {
|
||||
session_id: session.id,
|
||||
name: "frontend__echo".to_string(),
|
||||
arguments: Value::Object(serde_json::Map::new()),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("frontend tools should not be callable through /agent/call_tool"),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
assert_eq!(error.status, StatusCode::FAILED_DEPENDENCY);
|
||||
assert!(error.message.contains("frontend host"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user