diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 46b7f2d91b..54ad4a15f0 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -3,6 +3,7 @@ use goose::agents::extension::ToolInfo; use goose::agents::ExtensionConfig; use goose::config::permission::PermissionLevel; use goose::config::ExtensionEntry; +use goose::permission::permission_confirmation::PrincipalType; use goose::providers::base::ConfigKey; use goose::providers::base::ProviderMetadata; use mcp_core::tool::{Tool, ToolAnnotations}; @@ -23,6 +24,7 @@ use utoipa::OpenApi; super::routes::config_management::providers, super::routes::config_management::upsert_permissions, super::routes::agent::get_tools, + super::routes::reply::confirm_permission, ), components(schemas( super::routes::config_management::UpsertConfigQuery, @@ -34,6 +36,7 @@ use utoipa::OpenApi; super::routes::config_management::ExtensionQuery, super::routes::config_management::ToolPermission, super::routes::config_management::UpsertPermissionsQuery, + super::routes::reply::PermissionConfirmationRequest, ProviderMetadata, ExtensionEntry, ExtensionConfig, @@ -43,6 +46,7 @@ use utoipa::OpenApi; ToolAnnotations, ToolInfo, PermissionLevel, + PrincipalType, )) )] pub struct ApiDoc; diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index 787b8a6ab3..66c3dc8b62 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -31,6 +31,7 @@ use std::{ use tokio::sync::mpsc; use tokio::time::timeout; use tokio_stream::wrappers::ReceiverStream; +use utoipa::ToSchema; // Direct message serialization for the chat request #[derive(Debug, Deserialize)] @@ -365,10 +366,9 @@ async fn ask_handler( })) } -#[derive(Debug, Deserialize, Serialize)] -struct PermissionConfirmationRequest { +#[derive(Debug, Deserialize, Serialize, ToSchema)] +pub struct PermissionConfirmationRequest { id: String, - confirmed: bool, #[serde(default = "default_principal_type")] principal_type: PrincipalType, action: String, @@ -378,7 +378,17 @@ fn default_principal_type() -> PrincipalType { PrincipalType::Tool } -async fn confirm_handler( +#[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, headers: HeaderMap, Json(request): Json, @@ -392,10 +402,6 @@ async fn confirm_handler( if secret_key != state.secret_key { return Err(StatusCode::UNAUTHORIZED); } - tracing::info!( - "Received confirmation request: {}", - serde_json::to_string_pretty(&request).unwrap() - ); let agent = state.agent.clone(); let agent = agent.read().await; @@ -471,7 +477,7 @@ pub fn routes(state: AppState) -> Router { Router::new() .route("/reply", post(handler)) .route("/ask", post(ask_handler)) - .route("/confirm", post(confirm_handler)) + .route("/confirm", post(confirm_permission)) .route("/tool_result", post(submit_tool_result)) .with_state(state) } diff --git a/crates/goose/src/permission/permission_confirmation.rs b/crates/goose/src/permission/permission_confirmation.rs index b33dd5912d..cf08870549 100644 --- a/crates/goose/src/permission/permission_confirmation.rs +++ b/crates/goose/src/permission/permission_confirmation.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum Permission { @@ -7,7 +8,7 @@ pub enum Permission { DenyOnce, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)] pub enum PrincipalType { Extension, Tool, diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index f384d06879..7aed0554f1 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -351,6 +351,40 @@ } } } + }, + "/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" + } + } + } } }, "components": { @@ -635,6 +669,24 @@ } } }, + "PermissionConfirmationRequest": { + "type": "object", + "required": [ + "id", + "action" + ], + "properties": { + "action": { + "type": "string" + }, + "id": { + "type": "string" + }, + "principal_type": { + "$ref": "#/components/schemas/PrincipalType" + } + } + }, "PermissionLevel": { "type": "string", "description": "Enum representing the possible permission levels for a tool.", @@ -644,6 +696,13 @@ "never_allow" ] }, + "PrincipalType": { + "type": "string", + "enum": [ + "Extension", + "Tool" + ] + }, "ProviderDetails": { "type": "object", "required": [ diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 239536dfb3..5cfb61fb85 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse } from './types.gen'; +import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ConfirmPermissionData } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -113,4 +113,15 @@ export const upsertConfig = (options: Opti ...options?.headers } }); +}; + +export const confirmPermission = (options: Options) => { + return (options.client ?? _heyApiClient).post({ + url: '/confirm', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } + }); }; \ No newline at end of file diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 0409a2e6a2..640bfa7095 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -100,11 +100,19 @@ export type ExtensionResponse = { extensions: Array; }; +export type PermissionConfirmationRequest = { + action: string; + id: string; + principal_type?: PrincipalType; +}; + /** * Enum representing the possible permission levels for a tool. */ export type PermissionLevel = 'always_allow' | 'ask_before' | 'never_allow'; +export type PrincipalType = 'Extension' | 'Tool'; + export type ProviderDetails = { /** * Indicates whether the provider is fully configured @@ -521,6 +529,31 @@ export type UpsertConfigResponses = { export type UpsertConfigResponse = UpsertConfigResponses[keyof UpsertConfigResponses]; +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 ClientOptions = { baseUrl: `${string}://${string}` | (string & {}); }; \ No newline at end of file diff --git a/ui/desktop/src/components/ExtensionConfirmation.tsx b/ui/desktop/src/components/ExtensionConfirmation.tsx index 0e9aa1cf8a..3f4376add9 100644 --- a/ui/desktop/src/components/ExtensionConfirmation.tsx +++ b/ui/desktop/src/components/ExtensionConfirmation.tsx @@ -1,20 +1,39 @@ import React, { useState } from 'react'; -import { ConfirmExtensionRequest } from '../utils/extensionConfirm'; import { snakeToTitleCase } from '../utils'; +import { confirmPermission } from '../api'; +interface ExtensionConfirmationProps { + isCancelledMessage: boolean; + isClicked: boolean; + extensionConfirmationId: string; + extensionName: string; +} export default function ExtensionConfirmation({ isCancelledMessage, isClicked, extensionConfirmationId, extensionName, -}) { +}: ExtensionConfirmationProps) { const [clicked, setClicked] = useState(isClicked); const [status, setStatus] = useState('unknown'); - const handleButtonClick = (confirmed) => { + const handleButtonClick = async (confirmed: boolean) => { setClicked(true); setStatus(confirmed ? 'approved' : 'denied'); - ConfirmExtensionRequest(extensionConfirmationId, confirmed); + try { + const response = await confirmPermission({ + body: { + id: extensionConfirmationId, + action: confirmed ? 'allow_once' : 'deny', + principal_type: 'Extension', + }, + }); + if (response.error) { + console.error('Failed to confirm permission: ', response.error); + } + } catch (err) { + console.error('Error fetching tools:', err); + } }; return isCancelledMessage ? ( diff --git a/ui/desktop/src/components/ToolCallConfirmation.tsx b/ui/desktop/src/components/ToolCallConfirmation.tsx index 6b4d89b4a3..606923a316 100644 --- a/ui/desktop/src/components/ToolCallConfirmation.tsx +++ b/ui/desktop/src/components/ToolCallConfirmation.tsx @@ -1,8 +1,8 @@ import { useState } from 'react'; -import { ConfirmToolRequest } from '../utils/toolConfirm'; import { snakeToTitleCase } from '../utils'; import PermissionModal from './settings_v2/permission/PermissionModal'; import { ChevronRight } from 'lucide-react'; +import { confirmPermission } from '../api'; const ALWAYS_ALLOW = 'always_allow'; const ALLOW_ONCE = 'allow_once'; @@ -26,7 +26,7 @@ export default function ToolConfirmation({ const [actionDisplay, setActionDisplay] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); - const handleButtonClick = (action: string) => { + const handleButtonClick = async (action: string) => { setClicked(true); setStatus(action); if (action === ALWAYS_ALLOW) { @@ -36,7 +36,16 @@ export default function ToolConfirmation({ } else { setActionDisplay('denied'); } - ConfirmToolRequest(toolConfirmationId, action); + try { + const response = await confirmPermission({ + body: { id: toolConfirmationId, action, principal_type: 'Tool' }, + }); + if (response.error) { + console.error('Failed to confirm permission: ', response.error); + } + } catch (err) { + console.error('Error fetching tools:', err); + } }; const handleModalClose = () => { diff --git a/ui/desktop/src/utils/extensionConfirm.ts b/ui/desktop/src/utils/extensionConfirm.ts deleted file mode 100644 index d85cba7058..0000000000 --- a/ui/desktop/src/utils/extensionConfirm.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getApiUrl, getSecretKey } from '../config'; - -export async function ConfirmExtensionRequest(requestId: string, confirmed: boolean) { - try { - const response = await fetch(getApiUrl('/confirm'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - id: requestId, - confirmed, - principal_type: 'Extension', - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('Delete response error: ', errorText); - throw new Error('Failed to confirm extension enablement'); - } - } catch (error) { - console.error('Error confirming extension enablement: ', error); - } -} diff --git a/ui/desktop/src/utils/toolConfirm.ts b/ui/desktop/src/utils/toolConfirm.ts deleted file mode 100644 index d858654d11..0000000000 --- a/ui/desktop/src/utils/toolConfirm.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getApiUrl, getSecretKey } from '../config'; - -export async function ConfirmToolRequest(requesyId: string, action: string) { - try { - const response = await fetch(getApiUrl('/confirm'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - id: requesyId, - action, - principal_type: 'Tool', - }), - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error('Delete response error: ', errorText); - throw new Error('Failed to confirm tool'); - } - } catch (error) { - console.error('Error confirm tool: ', error); - } -}