feat: ActionRequired (#5897)

This commit is contained in:
Alex Hancock
2025-12-02 11:48:28 -05:00
committed by GitHub
parent bf188cd9e2
commit 131c7e7228
22 changed files with 428 additions and 224 deletions
+11 -1
View File
@@ -1,4 +1,6 @@
use goose::conversation::message::{Message, MessageContent, ToolRequest, ToolResponse}; use goose::conversation::message::{
ActionRequiredData, Message, MessageContent, ToolRequest, ToolResponse,
};
use goose::utils::safe_truncate; use goose::utils::safe_truncate;
use rmcp::model::{RawContent, ResourceContents, Role}; use rmcp::model::{RawContent, ResourceContents, Role};
use serde_json::Value; use serde_json::Value;
@@ -340,6 +342,14 @@ pub fn message_to_markdown(message: &Message, export_all_content: bool) -> Strin
let mut md = String::new(); let mut md = String::new();
for content in &message.content { for content in &message.content {
match content { match content {
MessageContent::ActionRequired(action) => match &action.data {
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
md.push_str(&format!(
"**Action Required** (tool_confirmation): {}\n\n",
tool_name
));
}
},
MessageContent::Text(text) => { MessageContent::Text(text) => {
md.push_str(&text.text); md.push_str(&text.text);
md.push_str("\n\n"); md.push_str("\n\n");
+23 -12
View File
@@ -40,7 +40,7 @@ use rmcp::model::ServerNotification;
use rmcp::model::{ErrorCode, ErrorData}; use rmcp::model::{ErrorCode, ErrorData};
use goose::config::paths::Paths; use goose::config::paths::Paths;
use goose::conversation::message::{Message, MessageContent}; use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
use rand::{distributions::Alphanumeric, Rng}; use rand::{distributions::Alphanumeric, Rng};
use rustyline::EditMode; use rustyline::EditMode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -852,12 +852,24 @@ impl CliSession {
result = stream.next() => { result = stream.next() => {
match result { match result {
Some(Ok(AgentEvent::Message(message))) => { Some(Ok(AgentEvent::Message(message))) => {
// If it's a confirmation request, get approval but otherwise do not render/persist let tool_call_confirmation = message.content.iter().find_map(|content| {
if let Some(MessageContent::ToolConfirmationRequest(confirmation)) = message.content.first() { if let MessageContent::ActionRequired(action) = content {
#[allow(irrefutable_let_patterns)] // this is a one variant enum right now but it will have more
if let ActionRequiredData::ToolConfirmation { id, tool_name, arguments, prompt } = &action.data {
Some((id.clone(), tool_name.clone(), arguments.clone(), prompt.clone()))
} else {
None
}
} else {
None
}
});
if let Some((id, _tool_name, _arguments, security_prompt)) = tool_call_confirmation {
output::hide_thinking(); output::hide_thinking();
// Format the confirmation prompt - use security message if present, otherwise use generic message // Format the confirmation prompt - use security message if present, otherwise use generic message
let prompt = if let Some(security_message) = &confirmation.prompt { let prompt = if let Some(security_message) = &security_prompt {
println!("\n{}", security_message); println!("\n{}", security_message);
"Do you allow this tool call?".to_string() "Do you allow this tool call?".to_string()
} else { } else {
@@ -865,7 +877,7 @@ impl CliSession {
}; };
// Get confirmation from user // Get confirmation from user
let permission_result = if confirmation.prompt.is_none() { let permission_result = if security_prompt.is_none() {
// No security message - show all options including "Always Allow" // No security message - show all options including "Always Allow"
cliclack::select(prompt) cliclack::select(prompt)
.item(Permission::AllowOnce, "Allow", "Allow the tool call once") .item(Permission::AllowOnce, "Allow", "Allow the tool call once")
@@ -883,13 +895,12 @@ impl CliSession {
}; };
let permission = match permission_result { let permission = match permission_result {
Ok(p) => p, // If Ok, use the selected permission Ok(p) => p,
Err(e) => { Err(e) => {
// Check if the error is an interruption (Ctrl+C/Cmd+C, Escape)
if e.kind() == std::io::ErrorKind::Interrupted { if e.kind() == std::io::ErrorKind::Interrupted {
Permission::Cancel // If interrupted, set permission to Cancel Permission::Cancel
} else { } else {
return Err(e.into()); // Otherwise, convert and propagate the original error return Err(e.into());
} }
} }
}; };
@@ -899,7 +910,7 @@ impl CliSession {
let mut response_message = Message::user(); let mut response_message = Message::user();
response_message.content.push(MessageContent::tool_response( response_message.content.push(MessageContent::tool_response(
confirmation.id.clone(), id.clone(),
Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: std::borrow::Cow::from("Tool call cancelled by user".to_string()), data: None }) Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: std::borrow::Cow::from("Tool call cancelled by user".to_string()), data: None })
)); ));
self.messages.push(response_message); self.messages.push(response_message);
@@ -907,10 +918,10 @@ impl CliSession {
drop(stream); drop(stream);
break; break;
} else { } else {
self.agent.handle_confirmation(confirmation.id.clone(), PermissionConfirmation { self.agent.handle_confirmation(id.clone(), PermissionConfirmation {
principal_type: PrincipalType::Tool, principal_type: PrincipalType::Tool,
permission, permission,
},).await; }).await;
} }
} }
else { else {
+8 -1
View File
@@ -2,7 +2,9 @@ use anstream::println;
use bat::WrappingMode; use bat::WrappingMode;
use console::{measure_text_width, style, Color, Term}; use console::{measure_text_width, style, Color, Term};
use goose::config::Config; use goose::config::Config;
use goose::conversation::message::{Message, MessageContent, ToolRequest, ToolResponse}; use goose::conversation::message::{
ActionRequiredData, Message, MessageContent, ToolRequest, ToolResponse,
};
use goose::providers::pricing::get_model_pricing; use goose::providers::pricing::get_model_pricing;
use goose::providers::pricing::parse_model_id; use goose::providers::pricing::parse_model_id;
use goose::utils::safe_truncate; use goose::utils::safe_truncate;
@@ -166,6 +168,11 @@ pub fn render_message(message: &Message, debug: bool) {
for content in &message.content { for content in &message.content {
match content { match content {
MessageContent::ActionRequired(action) => match &action.data {
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
println!("action_required(tool_confirmation): {}", tool_name)
}
},
MessageContent::Text(text) => print_markdown(&text.text, theme), MessageContent::Text(text) => print_markdown(&text.text, theme),
MessageContent::ToolRequest(req) => render_tool_request(req, theme, debug), MessageContent::ToolRequest(req) => render_tool_request(req, theme, debug),
MessageContent::ToolResponse(resp) => render_tool_response(resp, theme, debug), MessageContent::ToolResponse(resp) => render_tool_response(resp, theme, debug),
+7 -5
View File
@@ -19,9 +19,9 @@ use goose::config::declarative_providers::{
DeclarativeProviderConfig, LoadedProvider, ProviderEngine, DeclarativeProviderConfig, LoadedProvider, ProviderEngine,
}; };
use goose::conversation::message::{ use goose::conversation::message::{
FrontendToolRequest, Message, MessageContent, MessageMetadata, RedactedThinkingContent, ActionRequired, ActionRequiredData, FrontendToolRequest, Message, MessageContent,
SystemNotificationContent, SystemNotificationType, ThinkingContent, TokenState, MessageMetadata, RedactedThinkingContent, SystemNotificationContent, SystemNotificationType,
ToolConfirmationRequest, ToolRequest, ToolResponse, ThinkingContent, TokenState, ToolConfirmationRequest, ToolRequest, ToolResponse,
}; };
use crate::routes::recipe_utils::RecipeManifest; use crate::routes::recipe_utils::RecipeManifest;
@@ -358,7 +358,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::agent::agent_remove_extension, super::routes::agent::agent_remove_extension,
super::routes::agent::update_agent_provider, super::routes::agent::update_agent_provider,
super::routes::agent::update_router_tool_selector, super::routes::agent::update_router_tool_selector,
super::routes::reply::confirm_permission, super::routes::action_required::confirm_tool_action,
super::routes::reply::reply, super::routes::reply::reply,
super::routes::session::list_sessions, super::routes::session::list_sessions,
super::routes::session::get_session, super::routes::session::get_session,
@@ -411,7 +411,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::config_management::UpdateCustomProviderRequest, super::routes::config_management::UpdateCustomProviderRequest,
super::routes::config_management::CheckProviderRequest, super::routes::config_management::CheckProviderRequest,
super::routes::config_management::SetProviderRequest, super::routes::config_management::SetProviderRequest,
super::routes::reply::PermissionConfirmationRequest, super::routes::action_required::ConfirmToolActionRequest,
super::routes::reply::ChatRequest, super::routes::reply::ChatRequest,
super::routes::session::ImportSessionRequest, super::routes::session::ImportSessionRequest,
super::routes::session::SessionListResponse, super::routes::session::SessionListResponse,
@@ -438,6 +438,8 @@ derive_utoipa!(Icon as IconSchema);
ToolResponse, ToolResponse,
ToolRequest, ToolRequest,
ToolConfirmationRequest, ToolConfirmationRequest,
ActionRequired,
ActionRequiredData,
ThinkingContent, ThinkingContent,
RedactedThinkingContent, RedactedThinkingContent,
FrontendToolRequest, FrontendToolRequest,
@@ -0,0 +1,104 @@
use crate::state::AppState;
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use goose::permission::permission_confirmation::PrincipalType;
use goose::permission::{Permission, PermissionConfirmation};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
use utoipa::ToSchema;
#[derive(Debug, Deserialize, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ConfirmToolActionRequest {
id: String,
#[serde(default = "default_principal_type")]
principal_type: PrincipalType,
action: String,
session_id: String,
}
fn default_principal_type() -> PrincipalType {
PrincipalType::Tool
}
#[utoipa::path(
post,
path = "/action-required/tool-confirmation",
request_body = ConfirmToolActionRequest,
responses(
(status = 200, description = "Tool confirmation action is confirmed", body = Value),
(status = 401, description = "Unauthorized - invalid secret key"),
(status = 500, description = "Internal server error")
)
)]
pub async fn confirm_tool_action(
State(state): State<Arc<AppState>>,
Json(request): Json<ConfirmToolActionRequest>,
) -> Result<Json<Value>, StatusCode> {
let agent = state.get_agent_for_route(request.session_id).await?;
let permission = match request.action.as_str() {
"always_allow" => Permission::AlwaysAllow,
"allow_once" => Permission::AllowOnce,
"deny" => Permission::DenyOnce,
_ => Permission::DenyOnce,
};
agent
.handle_confirmation(
request.id.clone(),
PermissionConfirmation {
principal_type: request.principal_type,
permission,
},
)
.await;
Ok(Json(Value::Object(serde_json::Map::new())))
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route(
"/action-required/tool-confirmation",
post(confirm_tool_action),
)
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
mod integration_tests {
use super::*;
use axum::{body::Body, http::Request};
use tower::ServiceExt;
#[tokio::test(flavor = "multi_thread")]
async fn test_tool_confirmation_endpoint() {
let state = AppState::new().await.unwrap();
let app = routes(state);
let request = Request::builder()
.uri("/action-required/tool-confirmation")
.method("POST")
.header("content-type", "application/json")
.header("x-secret-key", "test-secret")
.body(Body::from(
serde_json::to_string(&ConfirmToolActionRequest {
id: "test-id".to_string(),
principal_type: PrincipalType::Tool,
action: "allow_once".to_string(),
session_id: "test-session".to_string(),
})
.unwrap(),
))
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}
}
+2
View File
@@ -1,3 +1,4 @@
pub mod action_required;
pub mod agent; pub mod agent;
pub mod audio; pub mod audio;
pub mod config_management; pub mod config_management;
@@ -22,6 +23,7 @@ pub fn configure(state: Arc<crate::state::AppState>, secret_key: String) -> Rout
Router::new() Router::new()
.merge(status::routes()) .merge(status::routes())
.merge(reply::routes(state.clone())) .merge(reply::routes(state.clone()))
.merge(action_required::routes(state.clone()))
.merge(agent::routes(state.clone())) .merge(agent::routes(state.clone()))
.merge(audio::routes(state.clone())) .merge(audio::routes(state.clone()))
.merge(config_management::routes(state.clone())) .merge(config_management::routes(state.clone()))
+1 -55
View File
@@ -8,17 +8,12 @@ use axum::{
}; };
use bytes::Bytes; use bytes::Bytes;
use futures::{stream::StreamExt, Stream}; use futures::{stream::StreamExt, Stream};
use goose::agents::{AgentEvent, SessionConfig};
use goose::conversation::message::{Message, MessageContent, TokenState}; use goose::conversation::message::{Message, MessageContent, TokenState};
use goose::conversation::Conversation; use goose::conversation::Conversation;
use goose::permission::{Permission, PermissionConfirmation};
use goose::session::SessionManager; use goose::session::SessionManager;
use goose::{
agents::{AgentEvent, SessionConfig},
permission::permission_confirmation::PrincipalType,
};
use rmcp::model::ServerNotification; use rmcp::model::ServerNotification;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{ use std::{
convert::Infallible, convert::Infallible,
pin::Pin, pin::Pin,
@@ -30,7 +25,6 @@ use tokio::sync::mpsc;
use tokio::time::timeout; use tokio::time::timeout;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use utoipa::ToSchema;
fn track_tool_telemetry(content: &MessageContent, all_messages: &[Message]) { fn track_tool_telemetry(content: &MessageContent, all_messages: &[Message]) {
match content { match content {
@@ -452,60 +446,12 @@ pub async fn reply(
Ok(SseResponse::new(stream)) Ok(SseResponse::new(stream))
} }
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct PermissionConfirmationRequest {
id: String,
#[serde(default = "default_principal_type")]
principal_type: PrincipalType,
action: String,
session_id: String,
}
fn default_principal_type() -> PrincipalType {
PrincipalType::Tool
}
#[utoipa::path(
post,
path = "/confirm",
request_body = PermissionConfirmationRequest,
responses(
(status = 200, description = "Permission action is confirmed", body = Value),
(status = 401, description = "Unauthorized - invalid secret key"),
(status = 500, description = "Internal server error")
)
)]
pub async fn confirm_permission(
State(state): State<Arc<AppState>>,
Json(request): Json<PermissionConfirmationRequest>,
) -> Result<Json<Value>, StatusCode> {
let agent = state.get_agent_for_route(request.session_id).await?;
let permission = match request.action.as_str() {
"always_allow" => Permission::AlwaysAllow,
"allow_once" => Permission::AllowOnce,
"deny" => Permission::DenyOnce,
_ => Permission::DenyOnce,
};
agent
.handle_confirmation(
request.id.clone(),
PermissionConfirmation {
principal_type: request.principal_type,
permission,
},
)
.await;
Ok(Json(Value::Object(serde_json::Map::new())))
}
pub fn routes(state: Arc<AppState>) -> Router { pub fn routes(state: Arc<AppState>) -> Router {
Router::new() Router::new()
.route( .route(
"/reply", "/reply",
post(reply).layer(DefaultBodyLimit::max(50 * 1024 * 1024)), post(reply).layer(DefaultBodyLimit::max(50 * 1024 * 1024)),
) )
.route("/confirm", post(confirm_permission))
.with_state(state) .with_state(state)
} }
+1 -1
View File
@@ -73,7 +73,7 @@ impl Agent {
}); });
let confirmation = Message::assistant() let confirmation = Message::assistant()
.with_tool_confirmation_request( .with_action_required(
request.id.clone(), request.id.clone(),
tool_call.name.to_string().clone(), tool_call.name.to_string().clone(),
tool_call.arguments.clone().unwrap_or_default(), tool_call.arguments.clone().unwrap_or_default(),
+8
View File
@@ -375,6 +375,14 @@ fn format_message_for_compacting(msg: &Message) -> String {
MessageContent::ToolConfirmationRequest(req) => { MessageContent::ToolConfirmationRequest(req) => {
format!("tool_confirmation_request: {}", req.tool_name) format!("tool_confirmation_request: {}", req.tool_name)
} }
MessageContent::ActionRequired(action) => match &action.data {
crate::conversation::message::ActionRequiredData::ToolConfirmation {
tool_name,
..
} => {
format!("action_required(tool_confirmation): {}", tool_name)
}
},
MessageContent::FrontendToolRequest(req) => { MessageContent::FrontendToolRequest(req) => {
if let Ok(call) = &req.tool_call { if let Ok(call) = &req.tool_call {
format!("frontend_tool_request: {}", call.name) format!("frontend_tool_request: {}", call.name)
+38 -12
View File
@@ -101,6 +101,24 @@ pub struct ToolConfirmationRequest {
pub prompt: Option<String>, pub prompt: Option<String>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(tag = "actionType", rename_all = "camelCase")]
pub enum ActionRequiredData {
#[serde(rename_all = "camelCase")]
ToolConfirmation {
id: String,
tool_name: String,
arguments: JsonObject,
prompt: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ActionRequired {
pub data: ActionRequiredData,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct ThinkingContent { pub struct ThinkingContent {
pub thinking: String, pub thinking: String,
@@ -144,6 +162,7 @@ pub enum MessageContent {
ToolRequest(ToolRequest), ToolRequest(ToolRequest),
ToolResponse(ToolResponse), ToolResponse(ToolResponse),
ToolConfirmationRequest(ToolConfirmationRequest), ToolConfirmationRequest(ToolConfirmationRequest),
ActionRequired(ActionRequired),
FrontendToolRequest(FrontendToolRequest), FrontendToolRequest(FrontendToolRequest),
Thinking(ThinkingContent), Thinking(ThinkingContent),
RedactedThinking(RedactedThinkingContent), RedactedThinking(RedactedThinkingContent),
@@ -169,6 +188,11 @@ impl fmt::Display for MessageContent {
MessageContent::ToolConfirmationRequest(r) => { MessageContent::ToolConfirmationRequest(r) => {
write!(f, "[ToolConfirmationRequest: {}]", r.tool_name) write!(f, "[ToolConfirmationRequest: {}]", r.tool_name)
} }
MessageContent::ActionRequired(a) => match &a.data {
ActionRequiredData::ToolConfirmation { tool_name, .. } => {
write!(f, "[ActionRequired: ToolConfirmation for {}]", tool_name)
}
},
MessageContent::FrontendToolRequest(r) => match &r.tool_call { MessageContent::FrontendToolRequest(r) => match &r.tool_call {
Ok(tool_call) => write!(f, "[FrontendToolRequest: {}]", tool_call.name), Ok(tool_call) => write!(f, "[FrontendToolRequest: {}]", tool_call.name),
Err(e) => write!(f, "[FrontendToolRequest: Error: {}]", e), Err(e) => write!(f, "[FrontendToolRequest: Error: {}]", e),
@@ -234,17 +258,19 @@ impl MessageContent {
}) })
} }
pub fn tool_confirmation_request<S: Into<String>>( pub fn action_required<S: Into<String>>(
id: S, id: S,
tool_name: String, tool_name: String,
arguments: JsonObject, arguments: JsonObject,
prompt: Option<String>, prompt: Option<String>,
) -> Self { ) -> Self {
MessageContent::ToolConfirmationRequest(ToolConfirmationRequest { MessageContent::ActionRequired(ActionRequired {
id: id.into(), data: ActionRequiredData::ToolConfirmation {
tool_name, id: id.into(),
arguments, tool_name,
prompt, arguments,
prompt,
},
}) })
} }
@@ -303,9 +329,9 @@ impl MessageContent {
} }
} }
pub fn as_tool_confirmation_request(&self) -> Option<&ToolConfirmationRequest> { pub fn as_action_required(&self) -> Option<&ActionRequired> {
if let MessageContent::ToolConfirmationRequest(ref tool_confirmation_request) = self { if let MessageContent::ActionRequired(ref action_required) = self {
Some(tool_confirmation_request) Some(action_required)
} else { } else {
None None
} }
@@ -582,15 +608,15 @@ impl Message {
self.with_content(MessageContent::tool_response(id, result)) self.with_content(MessageContent::tool_response(id, result))
} }
/// Add a tool confirmation request to the message /// Add an action required message for tool confirmation
pub fn with_tool_confirmation_request<S: Into<String>>( pub fn with_action_required<S: Into<String>>(
self, self,
id: S, id: S,
tool_name: String, tool_name: String,
arguments: JsonObject, arguments: JsonObject,
prompt: Option<String>, prompt: Option<String>,
) -> Self { ) -> Self {
self.with_content(MessageContent::tool_confirmation_request( self.with_content(MessageContent::action_required(
id, tool_name, arguments, prompt, id, tool_name, arguments, prompt,
)) ))
} }
@@ -91,6 +91,9 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => { MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => {
// Skip tool confirmation requests // Skip tool confirmation requests
} }
MessageContent::ActionRequired(_action_required) => {
// Skip action required messages - they're for UI only
}
MessageContent::SystemNotification(_) => { MessageContent::SystemNotification(_) => {
// Skip // Skip
} }
@@ -37,6 +37,9 @@ pub fn to_bedrock_message_content(content: &MessageContent) -> Result<bedrock::C
MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => { MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => {
bedrock::ContentBlock::Text("".to_string()) bedrock::ContentBlock::Text("".to_string())
} }
MessageContent::ActionRequired(_action_required) => {
bedrock::ContentBlock::Text("".to_string())
}
MessageContent::Image(image) => { MessageContent::Image(image) => {
bedrock::ContentBlock::Image(to_bedrock_image(&image.data, &image.mime_type)?) bedrock::ContentBlock::Image(to_bedrock_image(&image.data, &image.mime_type)?)
} }
@@ -208,9 +208,8 @@ fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<Data
} }
} }
} }
MessageContent::ToolConfirmationRequest(_) => { MessageContent::ToolConfirmationRequest(_) => {}
// Skip tool confirmation requests MessageContent::ActionRequired(_) => {}
}
MessageContent::Image(image) => { MessageContent::Image(image) => {
content_array.push(convert_image(image, image_format)); content_array.push(convert_image(image, image_format));
} }
+9 -7
View File
@@ -19,10 +19,12 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
.iter() .iter()
.filter(|m| m.is_agent_visible()) .filter(|m| m.is_agent_visible())
.filter(|message| { .filter(|message| {
message message.content.iter().any(|content| {
.content !matches!(
.iter() content,
.any(|content| !matches!(content, MessageContent::ToolConfirmationRequest(_))) MessageContent::ToolConfirmationRequest(_) | MessageContent::ActionRequired(_)
)
})
}) })
.map(|message| { .map(|message| {
let role = if message.role == Role::User { let role = if message.role == Role::User {
@@ -408,11 +410,11 @@ mod tests {
) )
} }
fn set_up_tool_confirmation_message(id: &str, tool_call: CallToolRequestParam) -> Message { fn set_up_action_required_message(id: &str, tool_call: CallToolRequestParam) -> Message {
Message::new( Message::new(
Role::User, Role::User,
0, 0,
vec![MessageContent::tool_confirmation_request( vec![MessageContent::action_required(
id.to_string(), id.to_string(),
tool_call.name.to_string().clone(), tool_call.name.to_string().clone(),
tool_call.arguments.unwrap_or_default().clone(), tool_call.arguments.unwrap_or_default().clone(),
@@ -474,7 +476,7 @@ mod tests {
arguments: Some(object(arguments.clone())), arguments: Some(object(arguments.clone())),
}, },
), ),
set_up_tool_confirmation_message( set_up_action_required_message(
"id2", "id2",
CallToolRequestParam { CallToolRequestParam {
name: "tool_name_2".into(), name: "tool_name_2".into(),
+2 -3
View File
@@ -198,9 +198,8 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec<
} }
} }
} }
MessageContent::ToolConfirmationRequest(_) => { MessageContent::ToolConfirmationRequest(_) => {}
// Skip tool confirmation requests MessageContent::ActionRequired(_) => {}
}
MessageContent::Image(image) => { MessageContent::Image(image) => {
content_array.push(convert_image(image, image_format)); content_array.push(convert_image(image, image_format));
} }
@@ -50,9 +50,8 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
} }
} }
} }
MessageContent::ToolConfirmationRequest(_) => { MessageContent::ToolConfirmationRequest(_) => {}
// Skip tool confirmation requests MessageContent::ActionRequired(_) => {}
}
MessageContent::SystemNotification(_) => { MessageContent::SystemNotification(_) => {
// Skip // Skip
} }
+125 -56
View File
@@ -13,6 +13,40 @@
"version": "1.15.0" "version": "1.15.0"
}, },
"paths": { "paths": {
"/action-required/tool-confirmation": {
"post": {
"tags": [
"super::routes::action_required"
],
"operationId": "confirm_tool_action",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfirmToolActionRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Tool confirmation action is confirmed",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Unauthorized - invalid secret key"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/agent/add_extension": { "/agent/add_extension": {
"post": { "post": {
"tags": [ "tags": [
@@ -954,40 +988,6 @@
} }
} }
}, },
"/confirm": {
"post": {
"tags": [
"super::routes::reply"
],
"operationId": "confirm_permission",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PermissionConfirmationRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Permission action is confirmed",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Unauthorized - invalid secret key"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/diagnostics/{session_id}": { "/diagnostics/{session_id}": {
"get": { "get": {
"tags": [ "tags": [
@@ -2353,6 +2353,54 @@
}, },
"components": { "components": {
"schemas": { "schemas": {
"ActionRequired": {
"type": "object",
"required": [
"data"
],
"properties": {
"data": {
"$ref": "#/components/schemas/ActionRequiredData"
}
}
},
"ActionRequiredData": {
"oneOf": [
{
"type": "object",
"required": [
"id",
"toolName",
"arguments",
"actionType"
],
"properties": {
"actionType": {
"type": "string",
"enum": [
"toolConfirmation"
]
},
"arguments": {
"$ref": "#/components/schemas/JsonObject"
},
"id": {
"type": "string"
},
"prompt": {
"type": "string",
"nullable": true
},
"toolName": {
"type": "string"
}
}
}
],
"discriminator": {
"propertyName": "actionType"
}
},
"AddExtensionRequest": { "AddExtensionRequest": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -2516,6 +2564,28 @@
} }
} }
}, },
"ConfirmToolActionRequest": {
"type": "object",
"required": [
"id",
"action",
"sessionId"
],
"properties": {
"action": {
"type": "string"
},
"id": {
"type": "string"
},
"principalType": {
"$ref": "#/components/schemas/PrincipalType"
},
"sessionId": {
"type": "string"
}
}
},
"Content": { "Content": {
"oneOf": [ "oneOf": [
{ {
@@ -3518,6 +3588,27 @@
} }
] ]
}, },
{
"allOf": [
{
"$ref": "#/components/schemas/ActionRequired"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"actionRequired"
]
}
}
}
]
},
{ {
"allOf": [ "allOf": [
{ {
@@ -3872,28 +3963,6 @@
} }
} }
}, },
"PermissionConfirmationRequest": {
"type": "object",
"required": [
"id",
"action",
"session_id"
],
"properties": {
"action": {
"type": "string"
},
"id": {
"type": "string"
},
"principal_type": {
"$ref": "#/components/schemas/PrincipalType"
},
"session_id": {
"type": "string"
}
}
},
"PermissionLevel": { "PermissionLevel": {
"type": "string", "type": "string",
"description": "Enum representing the possible permission levels for a tool.", "description": "Enum representing the possible permission levels for a tool.",
+12 -12
View File
@@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client'; import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen'; import { client } from './client.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/** /**
@@ -18,6 +18,17 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
meta?: Record<string, unknown>; meta?: Record<string, unknown>;
}; };
export const confirmToolAction = <ThrowOnError extends boolean = false>(options: Options<ConfirmToolActionData, ThrowOnError>) => {
return (options.client ?? client).post<ConfirmToolActionResponses, ConfirmToolActionErrors, ThrowOnError>({
url: '/action-required/tool-confirmation',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
export const agentAddExtension = <ThrowOnError extends boolean = false>(options: Options<AgentAddExtensionData, ThrowOnError>) => { export const agentAddExtension = <ThrowOnError extends boolean = false>(options: Options<AgentAddExtensionData, ThrowOnError>) => {
return (options.client ?? client).post<AgentAddExtensionResponses, AgentAddExtensionErrors, ThrowOnError>({ return (options.client ?? client).post<AgentAddExtensionResponses, AgentAddExtensionErrors, ThrowOnError>({
url: '/agent/add_extension', url: '/agent/add_extension',
@@ -285,17 +296,6 @@ export const validateConfig = <ThrowOnError extends boolean = false>(options?: O
}); });
}; };
export const confirmPermission = <ThrowOnError extends boolean = false>(options: Options<ConfirmPermissionData, ThrowOnError>) => {
return (options.client ?? client).post<ConfirmPermissionResponses, ConfirmPermissionErrors, ThrowOnError>({
url: '/confirm',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
};
export const diagnostics = <ThrowOnError extends boolean = false>(options: Options<DiagnosticsData, ThrowOnError>) => { export const diagnostics = <ThrowOnError extends boolean = false>(options: Options<DiagnosticsData, ThrowOnError>) => {
return (options.client ?? client).get<DiagnosticsResponses, DiagnosticsErrors, ThrowOnError>({ return (options.client ?? client).get<DiagnosticsResponses, DiagnosticsErrors, ThrowOnError>({
url: '/diagnostics/{session_id}', url: '/diagnostics/{session_id}',
+46 -32
View File
@@ -4,6 +4,18 @@ export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {}); baseUrl: `${string}://${string}` | (string & {});
}; };
export type ActionRequired = {
data: ActionRequiredData;
};
export type ActionRequiredData = {
actionType: 'toolConfirmation';
arguments: JsonObject;
id: string;
prompt?: string | null;
toolName: string;
};
export type AddExtensionRequest = { export type AddExtensionRequest = {
config: ExtensionConfig; config: ExtensionConfig;
session_id: string; session_id: string;
@@ -76,6 +88,13 @@ export type ConfigResponse = {
}; };
}; };
export type ConfirmToolActionRequest = {
action: string;
id: string;
principalType?: PrincipalType;
sessionId: string;
};
export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | RawAudioContent | RawResource; export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | RawAudioContent | RawResource;
export type Conversation = Array<Message>; export type Conversation = Array<Message>;
@@ -371,6 +390,8 @@ export type MessageContent = (TextContent & {
type: 'toolResponse'; type: 'toolResponse';
}) | (ToolConfirmationRequest & { }) | (ToolConfirmationRequest & {
type: 'toolConfirmationRequest'; type: 'toolConfirmationRequest';
}) | (ActionRequired & {
type: 'actionRequired';
}) | (FrontendToolRequest & { }) | (FrontendToolRequest & {
type: 'frontendToolRequest'; type: 'frontendToolRequest';
}) | (ThinkingContent & { }) | (ThinkingContent & {
@@ -471,13 +492,6 @@ export type ParseRecipeResponse = {
recipe: Recipe; recipe: Recipe;
}; };
export type PermissionConfirmationRequest = {
action: string;
id: string;
principal_type?: PrincipalType;
session_id: string;
};
/** /**
* Enum representing the possible permission levels for a tool. * Enum representing the possible permission levels for a tool.
*/ */
@@ -990,6 +1004,31 @@ export type UpsertPermissionsQuery = {
tool_permissions: Array<ToolPermission>; tool_permissions: Array<ToolPermission>;
}; };
export type ConfirmToolActionData = {
body: ConfirmToolActionRequest;
path?: never;
query?: never;
url: '/action-required/tool-confirmation';
};
export type ConfirmToolActionErrors = {
/**
* Unauthorized - invalid secret key
*/
401: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type ConfirmToolActionResponses = {
/**
* Tool confirmation action is confirmed
*/
200: unknown;
};
export type AgentAddExtensionData = { export type AgentAddExtensionData = {
body: AddExtensionRequest; body: AddExtensionRequest;
path?: never; path?: never;
@@ -1726,31 +1765,6 @@ export type ValidateConfigResponses = {
export type ValidateConfigResponse = ValidateConfigResponses[keyof ValidateConfigResponses]; export type ValidateConfigResponse = ValidateConfigResponses[keyof ValidateConfigResponses];
export type ConfirmPermissionData = {
body: PermissionConfirmationRequest;
path?: never;
query?: never;
url: '/confirm';
};
export type ConfirmPermissionErrors = {
/**
* Unauthorized - invalid secret key
*/
401: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type ConfirmPermissionResponses = {
/**
* Permission action is confirmed
*/
200: unknown;
};
export type DiagnosticsData = { export type DiagnosticsData = {
body?: never; body?: never;
path: { path: {
+8 -8
View File
@@ -11,7 +11,7 @@ import {
getToolConfirmationContent, getToolConfirmationContent,
NotificationEvent, NotificationEvent,
} from '../types/message'; } from '../types/message';
import { Message, confirmPermission } from '../api'; import { Message, confirmToolAction } from '../api';
import ToolCallConfirmation from './ToolCallConfirmation'; import ToolCallConfirmation from './ToolCallConfirmation';
import MessageCopyLink from './MessageCopyLink'; import MessageCopyLink from './MessageCopyLink';
import { cn } from '../utils'; import { cn } from '../utils';
@@ -100,22 +100,22 @@ export default function GooseMessage({
messageIndex === messageHistoryIndex - 1 && messageIndex === messageHistoryIndex - 1 &&
hasToolConfirmation && hasToolConfirmation &&
toolConfirmationContent && toolConfirmationContent &&
!handledToolConfirmations.current.has(toolConfirmationContent.id) !handledToolConfirmations.current.has(toolConfirmationContent.data.id)
) { ) {
const hasExistingResponse = messages.some((msg) => const hasExistingResponse = messages.some((msg) =>
getToolResponses(msg).some((response) => response.id === toolConfirmationContent.id) getToolResponses(msg).some((response) => response.id === toolConfirmationContent.data.id)
); );
if (!hasExistingResponse) { if (!hasExistingResponse) {
handledToolConfirmations.current.add(toolConfirmationContent.id); handledToolConfirmations.current.add(toolConfirmationContent.data.id);
void (async () => { void (async () => {
try { try {
await confirmPermission({ await confirmToolAction({
body: { body: {
session_id: sessionId, sessionId,
id: toolConfirmationContent.id,
action: 'deny', action: 'deny',
id: toolConfirmationContent.data.id,
}, },
throwOnError: true, throwOnError: true,
}); });
@@ -216,7 +216,7 @@ export default function GooseMessage({
sessionId={sessionId} sessionId={sessionId}
isCancelledMessage={messageIndex == messageHistoryIndex - 1} isCancelledMessage={messageIndex == messageHistoryIndex - 1}
isClicked={messageIndex < messageHistoryIndex} isClicked={messageIndex < messageHistoryIndex}
toolConfirmationContent={toolConfirmationContent} actionRequiredContent={toolConfirmationContent}
/> />
)} )}
</div> </div>
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { snakeToTitleCase } from '../utils'; import { snakeToTitleCase } from '../utils';
import PermissionModal from './settings/permission/PermissionModal'; import PermissionModal from './settings/permission/PermissionModal';
import { ChevronRight } from 'lucide-react'; import { ChevronRight } from 'lucide-react';
import { confirmPermission, ToolConfirmationRequest } from '../api'; import { confirmToolAction, ActionRequired } from '../api';
import { Button } from './ui/button'; import { Button } from './ui/button';
const ALLOW_ONCE = 'allow_once'; const ALLOW_ONCE = 'allow_once';
@@ -24,16 +24,16 @@ interface ToolConfirmationProps {
sessionId: string; sessionId: string;
isCancelledMessage: boolean; isCancelledMessage: boolean;
isClicked: boolean; isClicked: boolean;
toolConfirmationContent: ToolConfirmationRequest & { type: 'toolConfirmationRequest' }; actionRequiredContent: ActionRequired & { type: 'actionRequired' };
} }
export default function ToolConfirmation({ export default function ToolConfirmation({
sessionId, sessionId,
isCancelledMessage, isCancelledMessage,
isClicked, isClicked,
toolConfirmationContent, actionRequiredContent,
}: ToolConfirmationProps) { }: ToolConfirmationProps) {
const { id: toolConfirmationId, toolName, prompt } = toolConfirmationContent; const { id: toolConfirmationId, toolName, prompt } = actionRequiredContent.data;
// Check if we have a stored state for this tool confirmation // Check if we have a stored state for this tool confirmation
const storedState = toolConfirmationState.get(toolConfirmationId); const storedState = toolConfirmationState.get(toolConfirmationId);
@@ -96,19 +96,19 @@ export default function ToolConfirmation({
}); });
try { try {
const response = await confirmPermission({ const response = await confirmToolAction({
body: { body: {
session_id: sessionId, sessionId: sessionId,
id: toolConfirmationId, id: toolConfirmationId,
action: newStatus, action: newStatus,
principal_type: 'Tool', principalType: 'Tool',
}, },
}); });
if (response.error) { if (response.error) {
console.error('Failed to confirm permission:', response.error); console.error('Failed to confirm tool action:', response.error);
} }
} catch (err) { } catch (err) {
console.error('Error confirming permission:', err); console.error('Error confirming tool action:', err);
} }
}; };
+4 -4
View File
@@ -1,4 +1,4 @@
import { Message, MessageEvent, ToolConfirmationRequest, ToolRequest, ToolResponse } from '../api'; import { Message, MessageEvent, ActionRequired, ToolRequest, ToolResponse } from '../api';
export type ToolRequestMessageContent = ToolRequest & { type: 'toolRequest' }; export type ToolRequestMessageContent = ToolRequest & { type: 'toolRequest' };
export type ToolResponseMessageContent = ToolResponse & { type: 'toolResponse' }; export type ToolResponseMessageContent = ToolResponse & { type: 'toolResponse' };
@@ -44,10 +44,10 @@ export function getToolResponses(message: Message): (ToolResponse & { type: 'too
export function getToolConfirmationContent( export function getToolConfirmationContent(
message: Message message: Message
): (ToolConfirmationRequest & { type: 'toolConfirmationRequest' }) | undefined { ): (ActionRequired & { type: 'actionRequired' }) | undefined {
return message.content.find( return message.content.find(
(content): content is ToolConfirmationRequest & { type: 'toolConfirmationRequest' } => (content): content is ActionRequired & { type: 'actionRequired' } =>
content.type === 'toolConfirmationRequest' content.type === 'actionRequired' && content.data.actionType === 'toolConfirmation'
); );
} }