fix: resolve confirmation (#2161)

This commit is contained in:
Yingjie He
2025-04-11 13:58:54 -07:00
committed by GitHub
parent 454e4a47f4
commit 79e65ec3de
10 changed files with 160 additions and 70 deletions

View File

@@ -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;

View File

@@ -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<AppState>,
headers: HeaderMap,
Json(request): Json<PermissionConfirmationRequest>,
@@ -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)
}

View File

@@ -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,

View File

@@ -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": [

View File

@@ -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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@@ -114,3 +114,14 @@ export const upsertConfig = <ThrowOnError extends boolean = false>(options: Opti
}
});
};
export const confirmPermission = <ThrowOnError extends boolean = false>(options: Options<ConfirmPermissionData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).post<unknown, unknown, ThrowOnError>({
url: '/confirm',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});
};

View File

@@ -100,11 +100,19 @@ export type ExtensionResponse = {
extensions: Array<ExtensionEntry>;
};
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 & {});
};

View File

@@ -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 ? (

View File

@@ -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 = () => {

View File

@@ -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);
}
}

View File

@@ -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);
}
}