fix: resolve confirmation (#2161)
This commit is contained in:
@@ -3,6 +3,7 @@ use goose::agents::extension::ToolInfo;
|
|||||||
use goose::agents::ExtensionConfig;
|
use goose::agents::ExtensionConfig;
|
||||||
use goose::config::permission::PermissionLevel;
|
use goose::config::permission::PermissionLevel;
|
||||||
use goose::config::ExtensionEntry;
|
use goose::config::ExtensionEntry;
|
||||||
|
use goose::permission::permission_confirmation::PrincipalType;
|
||||||
use goose::providers::base::ConfigKey;
|
use goose::providers::base::ConfigKey;
|
||||||
use goose::providers::base::ProviderMetadata;
|
use goose::providers::base::ProviderMetadata;
|
||||||
use mcp_core::tool::{Tool, ToolAnnotations};
|
use mcp_core::tool::{Tool, ToolAnnotations};
|
||||||
@@ -23,6 +24,7 @@ use utoipa::OpenApi;
|
|||||||
super::routes::config_management::providers,
|
super::routes::config_management::providers,
|
||||||
super::routes::config_management::upsert_permissions,
|
super::routes::config_management::upsert_permissions,
|
||||||
super::routes::agent::get_tools,
|
super::routes::agent::get_tools,
|
||||||
|
super::routes::reply::confirm_permission,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
super::routes::config_management::UpsertConfigQuery,
|
super::routes::config_management::UpsertConfigQuery,
|
||||||
@@ -34,6 +36,7 @@ use utoipa::OpenApi;
|
|||||||
super::routes::config_management::ExtensionQuery,
|
super::routes::config_management::ExtensionQuery,
|
||||||
super::routes::config_management::ToolPermission,
|
super::routes::config_management::ToolPermission,
|
||||||
super::routes::config_management::UpsertPermissionsQuery,
|
super::routes::config_management::UpsertPermissionsQuery,
|
||||||
|
super::routes::reply::PermissionConfirmationRequest,
|
||||||
ProviderMetadata,
|
ProviderMetadata,
|
||||||
ExtensionEntry,
|
ExtensionEntry,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
@@ -43,6 +46,7 @@ use utoipa::OpenApi;
|
|||||||
ToolAnnotations,
|
ToolAnnotations,
|
||||||
ToolInfo,
|
ToolInfo,
|
||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
|
PrincipalType,
|
||||||
))
|
))
|
||||||
)]
|
)]
|
||||||
pub struct ApiDoc;
|
pub struct ApiDoc;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use std::{
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
// Direct message serialization for the chat request
|
// Direct message serialization for the chat request
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -365,10 +366,9 @@ async fn ask_handler(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||||
struct PermissionConfirmationRequest {
|
pub struct PermissionConfirmationRequest {
|
||||||
id: String,
|
id: String,
|
||||||
confirmed: bool,
|
|
||||||
#[serde(default = "default_principal_type")]
|
#[serde(default = "default_principal_type")]
|
||||||
principal_type: PrincipalType,
|
principal_type: PrincipalType,
|
||||||
action: String,
|
action: String,
|
||||||
@@ -378,7 +378,17 @@ fn default_principal_type() -> PrincipalType {
|
|||||||
PrincipalType::Tool
|
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>,
|
State(state): State<AppState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(request): Json<PermissionConfirmationRequest>,
|
Json(request): Json<PermissionConfirmationRequest>,
|
||||||
@@ -392,10 +402,6 @@ async fn confirm_handler(
|
|||||||
if secret_key != state.secret_key {
|
if secret_key != state.secret_key {
|
||||||
return Err(StatusCode::UNAUTHORIZED);
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
tracing::info!(
|
|
||||||
"Received confirmation request: {}",
|
|
||||||
serde_json::to_string_pretty(&request).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let agent = state.agent.clone();
|
let agent = state.agent.clone();
|
||||||
let agent = agent.read().await;
|
let agent = agent.read().await;
|
||||||
@@ -471,7 +477,7 @@ pub fn routes(state: AppState) -> Router {
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/reply", post(handler))
|
.route("/reply", post(handler))
|
||||||
.route("/ask", post(ask_handler))
|
.route("/ask", post(ask_handler))
|
||||||
.route("/confirm", post(confirm_handler))
|
.route("/confirm", post(confirm_permission))
|
||||||
.route("/tool_result", post(submit_tool_result))
|
.route("/tool_result", post(submit_tool_result))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||||
pub enum Permission {
|
pub enum Permission {
|
||||||
@@ -7,7 +8,7 @@ pub enum Permission {
|
|||||||
DenyOnce,
|
DenyOnce,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
|
||||||
pub enum PrincipalType {
|
pub enum PrincipalType {
|
||||||
Extension,
|
Extension,
|
||||||
Tool,
|
Tool,
|
||||||
|
|||||||
@@ -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": {
|
"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": {
|
"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.",
|
||||||
@@ -644,6 +696,13 @@
|
|||||||
"never_allow"
|
"never_allow"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"PrincipalType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Extension",
|
||||||
|
"Tool"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ProviderDetails": {
|
"ProviderDetails": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
|
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';
|
import { client as _heyApiClient } from './client.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||||
@@ -113,4 +113,15 @@ export const upsertConfig = <ThrowOnError extends boolean = false>(options: Opti
|
|||||||
...options?.headers
|
...options?.headers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
@@ -100,11 +100,19 @@ export type ExtensionResponse = {
|
|||||||
extensions: Array<ExtensionEntry>;
|
extensions: Array<ExtensionEntry>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PermissionConfirmationRequest = {
|
||||||
|
action: string;
|
||||||
|
id: string;
|
||||||
|
principal_type?: PrincipalType;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum representing the possible permission levels for a tool.
|
* Enum representing the possible permission levels for a tool.
|
||||||
*/
|
*/
|
||||||
export type PermissionLevel = 'always_allow' | 'ask_before' | 'never_allow';
|
export type PermissionLevel = 'always_allow' | 'ask_before' | 'never_allow';
|
||||||
|
|
||||||
|
export type PrincipalType = 'Extension' | 'Tool';
|
||||||
|
|
||||||
export type ProviderDetails = {
|
export type ProviderDetails = {
|
||||||
/**
|
/**
|
||||||
* Indicates whether the provider is fully configured
|
* Indicates whether the provider is fully configured
|
||||||
@@ -521,6 +529,31 @@ export type UpsertConfigResponses = {
|
|||||||
|
|
||||||
export type UpsertConfigResponse = UpsertConfigResponses[keyof 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 = {
|
export type ClientOptions = {
|
||||||
baseUrl: `${string}://${string}` | (string & {});
|
baseUrl: `${string}://${string}` | (string & {});
|
||||||
};
|
};
|
||||||
@@ -1,20 +1,39 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ConfirmExtensionRequest } from '../utils/extensionConfirm';
|
|
||||||
import { snakeToTitleCase } from '../utils';
|
import { snakeToTitleCase } from '../utils';
|
||||||
|
import { confirmPermission } from '../api';
|
||||||
|
|
||||||
|
interface ExtensionConfirmationProps {
|
||||||
|
isCancelledMessage: boolean;
|
||||||
|
isClicked: boolean;
|
||||||
|
extensionConfirmationId: string;
|
||||||
|
extensionName: string;
|
||||||
|
}
|
||||||
export default function ExtensionConfirmation({
|
export default function ExtensionConfirmation({
|
||||||
isCancelledMessage,
|
isCancelledMessage,
|
||||||
isClicked,
|
isClicked,
|
||||||
extensionConfirmationId,
|
extensionConfirmationId,
|
||||||
extensionName,
|
extensionName,
|
||||||
}) {
|
}: ExtensionConfirmationProps) {
|
||||||
const [clicked, setClicked] = useState(isClicked);
|
const [clicked, setClicked] = useState(isClicked);
|
||||||
const [status, setStatus] = useState('unknown');
|
const [status, setStatus] = useState('unknown');
|
||||||
|
|
||||||
const handleButtonClick = (confirmed) => {
|
const handleButtonClick = async (confirmed: boolean) => {
|
||||||
setClicked(true);
|
setClicked(true);
|
||||||
setStatus(confirmed ? 'approved' : 'denied');
|
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 ? (
|
return isCancelledMessage ? (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ConfirmToolRequest } from '../utils/toolConfirm';
|
|
||||||
import { snakeToTitleCase } from '../utils';
|
import { snakeToTitleCase } from '../utils';
|
||||||
import PermissionModal from './settings_v2/permission/PermissionModal';
|
import PermissionModal from './settings_v2/permission/PermissionModal';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
import { confirmPermission } from '../api';
|
||||||
|
|
||||||
const ALWAYS_ALLOW = 'always_allow';
|
const ALWAYS_ALLOW = 'always_allow';
|
||||||
const ALLOW_ONCE = 'allow_once';
|
const ALLOW_ONCE = 'allow_once';
|
||||||
@@ -26,7 +26,7 @@ export default function ToolConfirmation({
|
|||||||
const [actionDisplay, setActionDisplay] = useState('');
|
const [actionDisplay, setActionDisplay] = useState('');
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleButtonClick = (action: string) => {
|
const handleButtonClick = async (action: string) => {
|
||||||
setClicked(true);
|
setClicked(true);
|
||||||
setStatus(action);
|
setStatus(action);
|
||||||
if (action === ALWAYS_ALLOW) {
|
if (action === ALWAYS_ALLOW) {
|
||||||
@@ -36,7 +36,16 @@ export default function ToolConfirmation({
|
|||||||
} else {
|
} else {
|
||||||
setActionDisplay('denied');
|
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 = () => {
|
const handleModalClose = () => {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user