mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 22:54:24 +01:00
feat: support tool level permission control in ui (#2133)
This commit is contained in:
@@ -21,6 +21,7 @@ use utoipa::OpenApi;
|
||||
super::routes::config_management::get_extensions,
|
||||
super::routes::config_management::read_all_config,
|
||||
super::routes::config_management::providers,
|
||||
super::routes::config_management::upsert_permissions,
|
||||
super::routes::agent::get_tools,
|
||||
),
|
||||
components(schemas(
|
||||
@@ -31,6 +32,8 @@ use utoipa::OpenApi;
|
||||
super::routes::config_management::ProviderDetails,
|
||||
super::routes::config_management::ExtensionResponse,
|
||||
super::routes::config_management::ExtensionQuery,
|
||||
super::routes::config_management::ToolPermission,
|
||||
super::routes::config_management::UpsertPermissionsQuery,
|
||||
ProviderMetadata,
|
||||
ExtensionEntry,
|
||||
ExtensionConfig,
|
||||
|
||||
@@ -5,10 +5,13 @@ use axum::{
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use goose::agents::{extension::ToolInfo, extension_manager::get_parameter_names};
|
||||
use goose::config::Config;
|
||||
use goose::config::PermissionManager;
|
||||
use goose::{agents::Agent, model::ModelConfig, providers};
|
||||
use goose::{
|
||||
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
||||
config::permission::PermissionLevel,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
@@ -173,7 +176,7 @@ async fn list_providers() -> Json<Vec<ProviderList>> {
|
||||
("extension_name" = Option<String>, Query, description = "Optional extension name to filter tools")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Tools retrieved successfully", body = Vec<Tool>),
|
||||
(status = 200, description = "Tools retrieved successfully", body = Vec<ToolInfo>),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
(status = 500, description = "Internal server error")
|
||||
@@ -193,11 +196,13 @@ async fn get_tools(
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
let config = Config::global();
|
||||
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
|
||||
let mut agent = state.agent.write().await;
|
||||
let agent = agent.as_mut().ok_or(StatusCode::PRECONDITION_REQUIRED)?;
|
||||
let permission_manager = PermissionManager::default();
|
||||
|
||||
let tools = agent
|
||||
let mut tools: Vec<ToolInfo> = agent
|
||||
.list_tools()
|
||||
.await
|
||||
.into_iter()
|
||||
@@ -210,14 +215,27 @@ async fn get_tools(
|
||||
}
|
||||
})
|
||||
.map(|tool| {
|
||||
let permission = permission_manager
|
||||
.get_user_permission(&tool.name)
|
||||
.or_else(|| {
|
||||
if goose_mode == "smart_approve" {
|
||||
permission_manager.get_smart_approve_permission(&tool.name)
|
||||
} else if goose_mode == "approve" {
|
||||
Some(PermissionLevel::AskBefore)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
ToolInfo::new(
|
||||
&tool.name,
|
||||
&tool.description,
|
||||
get_parameter_names(&tool),
|
||||
permission_manager.get_user_permission(&tool.name),
|
||||
permission,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<ToolInfo>>();
|
||||
tools.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(Json(tools))
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ use axum::{
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use goose::agents::ExtensionConfig;
|
||||
use goose::config::extensions::name_to_key;
|
||||
use goose::config::Config;
|
||||
use goose::config::{extensions::name_to_key, PermissionManager};
|
||||
use goose::config::{ExtensionConfigManager, ExtensionEntry};
|
||||
use goose::providers::base::ProviderMetadata;
|
||||
use goose::providers::providers as get_providers;
|
||||
use goose::{agents::ExtensionConfig, config::permission::PermissionLevel};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -78,6 +78,18 @@ pub struct ProvidersResponse {
|
||||
pub providers: Vec<ProviderDetails>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ToolPermission {
|
||||
/// Unique identifier and name of the tool, format <extension_name>__<tool_name>
|
||||
pub tool_name: String,
|
||||
pub permission: PermissionLevel,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct UpsertPermissionsQuery {
|
||||
pub tool_permissions: Vec<ToolPermission>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/upsert",
|
||||
@@ -389,6 +401,34 @@ pub async fn init_config(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/permissions",
|
||||
request_body = UpsertPermissionsQuery,
|
||||
responses(
|
||||
(status = 200, description = "Permission update completed", body = String),
|
||||
(status = 400, description = "Invalid request"),
|
||||
)
|
||||
)]
|
||||
pub async fn upsert_permissions(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(query): Json<UpsertPermissionsQuery>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
verify_secret_key(&headers, &state)?;
|
||||
|
||||
let mut permission_manager = PermissionManager::default();
|
||||
// Iterate over each tool permission and update permissions
|
||||
for tool_permission in &query.tool_permissions {
|
||||
permission_manager.update_user_permission(
|
||||
&tool_permission.tool_name,
|
||||
tool_permission.permission.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Json("Permissions updated successfully".to_string()))
|
||||
}
|
||||
|
||||
pub fn routes(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/config", get(read_all_config))
|
||||
@@ -400,5 +440,6 @@ pub fn routes(state: AppState) -> Router {
|
||||
.route("/config/extensions/:name", delete(remove_extension))
|
||||
.route("/config/providers", get(providers))
|
||||
.route("/config/init", post(init_config))
|
||||
.route("/config/permissions", post(upsert_permissions))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ async fn ask_handler(
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ToolConfirmationRequest {
|
||||
id: String,
|
||||
confirmed: bool,
|
||||
action: String,
|
||||
}
|
||||
|
||||
async fn confirm_handler(
|
||||
@@ -389,11 +389,14 @@ async fn confirm_handler(
|
||||
let agent = state.agent.clone();
|
||||
let agent = agent.read().await;
|
||||
let agent = agent.as_ref().ok_or(StatusCode::NOT_FOUND)?;
|
||||
let permission = if request.confirmed {
|
||||
Permission::AllowOnce
|
||||
} else {
|
||||
Permission::DenyOnce
|
||||
|
||||
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(),
|
||||
|
||||
@@ -24,6 +24,8 @@ pub enum ExtensionError {
|
||||
Transport(#[from] mcp_client::transport::Error),
|
||||
#[error("Environment variable `{0}` is not allowed to be overridden.")]
|
||||
InvalidEnvVar(String),
|
||||
#[error("Join error occurred during task execution: {0}")]
|
||||
TaskJoinError(#[from] tokio::task::JoinError),
|
||||
}
|
||||
|
||||
pub type ExtensionResult<T> = Result<T, ExtensionError>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use futures::future;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use mcp_client::McpService;
|
||||
use mcp_core::protocol::GetPromptResult;
|
||||
@@ -8,6 +9,7 @@ use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task;
|
||||
use tracing::debug;
|
||||
|
||||
use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, ToolInfo};
|
||||
@@ -230,29 +232,47 @@ impl ExtensionManager {
|
||||
|
||||
/// Get all tools from all clients with proper prefixing
|
||||
pub async fn get_prefixed_tools(&self) -> ExtensionResult<Vec<Tool>> {
|
||||
let client_futures = self.clients.iter().map(|(name, client)| {
|
||||
let name = name.clone();
|
||||
let client = client.clone();
|
||||
|
||||
task::spawn(async move {
|
||||
let mut tools = Vec::new();
|
||||
let client_guard = client.lock().await;
|
||||
let mut client_tools = client_guard.list_tools(None).await?;
|
||||
|
||||
loop {
|
||||
for tool in client_tools.tools {
|
||||
tools.push(Tool::new(
|
||||
format!("{}__{}", name, tool.name),
|
||||
&tool.description,
|
||||
tool.input_schema,
|
||||
tool.annotations,
|
||||
));
|
||||
}
|
||||
|
||||
// Exit loop when there are no more pages
|
||||
if client_tools.next_cursor.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
client_tools = client_guard.list_tools(client_tools.next_cursor).await?;
|
||||
}
|
||||
|
||||
Ok::<Vec<Tool>, ExtensionError>(tools)
|
||||
})
|
||||
});
|
||||
|
||||
// Collect all results concurrently
|
||||
let results = future::join_all(client_futures).await;
|
||||
|
||||
// Aggregate tools and handle errors
|
||||
let mut tools = Vec::new();
|
||||
|
||||
// Add tools from MCP extensions with prefixing
|
||||
for (name, client) in &self.clients {
|
||||
let client_guard = client.lock().await;
|
||||
let mut client_tools = client_guard.list_tools(None).await?;
|
||||
|
||||
loop {
|
||||
for tool in client_tools.tools {
|
||||
tools.push(Tool::new(
|
||||
format!("{}__{}", name, tool.name),
|
||||
&tool.description,
|
||||
tool.input_schema,
|
||||
tool.annotations,
|
||||
));
|
||||
}
|
||||
|
||||
// exit loop when there are no more pages
|
||||
if client_tools.next_cursor.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
client_tools = client_guard.list_tools(client_tools.next_cursor).await?;
|
||||
for result in results {
|
||||
match result {
|
||||
Ok(Ok(client_tools)) => tools.extend(client_tools),
|
||||
Ok(Err(err)) => return Err(err),
|
||||
Err(join_err) => return Err(ExtensionError::from(join_err)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Tool"
|
||||
"$ref": "#/components/schemas/ToolInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/permissions": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "upsert_permissions",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpsertPermissionsQuery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Permission update completed",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/providers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -783,6 +816,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ToolPermission": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tool_name",
|
||||
"permission"
|
||||
],
|
||||
"properties": {
|
||||
"permission": {
|
||||
"$ref": "#/components/schemas/PermissionLevel"
|
||||
},
|
||||
"tool_name": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier and name of the tool, format <extension_name>__<tool_name>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpsertConfigQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -799,6 +848,20 @@
|
||||
},
|
||||
"value": {}
|
||||
}
|
||||
},
|
||||
"UpsertPermissionsQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tool_permissions"
|
||||
],
|
||||
"properties": {
|
||||
"tool_permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ToolPermission"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
943
ui/desktop/package-lock.json
generated
943
ui/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -82,12 +82,12 @@
|
||||
"@hey-api/client-fetch": "^0.8.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.7",
|
||||
"@radix-ui/react-icons": "^1.3.1",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.5",
|
||||
"@radix-ui/react-select": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.1.5",
|
||||
|
||||
@@ -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, 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 } from './types.gen';
|
||||
import { client as _heyApiClient } from './client.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@@ -64,6 +64,17 @@ export const initConfig = <ThrowOnError extends boolean = false>(options?: Optio
|
||||
});
|
||||
};
|
||||
|
||||
export const upsertPermissions = <ThrowOnError extends boolean = false>(options: Options<UpsertPermissionsData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<UpsertPermissionsResponse, unknown, ThrowOnError>({
|
||||
url: '/config/permissions',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const providers = <ThrowOnError extends boolean = false>(options?: Options<ProvidersData, ThrowOnError>) => {
|
||||
return (options?.client ?? _heyApiClient).get<ProvidersResponse2, unknown, ThrowOnError>({
|
||||
url: '/config/providers',
|
||||
|
||||
@@ -235,12 +235,24 @@ export type ToolInfo = {
|
||||
permission?: PermissionLevel | null;
|
||||
};
|
||||
|
||||
export type ToolPermission = {
|
||||
permission: PermissionLevel;
|
||||
/**
|
||||
* Unique identifier and name of the tool, format <extension_name>__<tool_name>
|
||||
*/
|
||||
tool_name: string;
|
||||
};
|
||||
|
||||
export type UpsertConfigQuery = {
|
||||
is_secret: boolean;
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
export type UpsertPermissionsQuery = {
|
||||
tool_permissions: Array<ToolPermission>;
|
||||
};
|
||||
|
||||
export type GetToolsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -272,7 +284,7 @@ export type GetToolsResponses = {
|
||||
/**
|
||||
* Tools retrieved successfully
|
||||
*/
|
||||
200: Array<Tool>;
|
||||
200: Array<ToolInfo>;
|
||||
};
|
||||
|
||||
export type GetToolsResponse = GetToolsResponses[keyof GetToolsResponses];
|
||||
@@ -399,6 +411,29 @@ export type InitConfigResponses = {
|
||||
|
||||
export type InitConfigResponse = InitConfigResponses[keyof InitConfigResponses];
|
||||
|
||||
export type UpsertPermissionsData = {
|
||||
body: UpsertPermissionsQuery;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/permissions';
|
||||
};
|
||||
|
||||
export type UpsertPermissionsErrors = {
|
||||
/**
|
||||
* Invalid request
|
||||
*/
|
||||
400: unknown;
|
||||
};
|
||||
|
||||
export type UpsertPermissionsResponses = {
|
||||
/**
|
||||
* Permission update completed
|
||||
*/
|
||||
200: string;
|
||||
};
|
||||
|
||||
export type UpsertPermissionsResponse = UpsertPermissionsResponses[keyof UpsertPermissionsResponses];
|
||||
|
||||
export type ProvidersData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
import React, { useState } from 'react';
|
||||
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';
|
||||
|
||||
const ALWAYS_ALLOW = 'always_allow';
|
||||
const ALLOW_ONCE = 'allow_once';
|
||||
const DENY = 'deny';
|
||||
|
||||
interface ToolConfirmationProps {
|
||||
isCancelledMessage: boolean;
|
||||
isClicked: boolean;
|
||||
toolConfirmationId: string;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export default function ToolConfirmation({
|
||||
isCancelledMessage,
|
||||
isClicked,
|
||||
toolConfirmationId,
|
||||
toolName,
|
||||
}) {
|
||||
}: ToolConfirmationProps) {
|
||||
const [clicked, setClicked] = useState(isClicked);
|
||||
const [status, setStatus] = useState('unknown');
|
||||
const [actionDisplay, setActionDisplay] = useState('');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const handleButtonClick = (confirmed) => {
|
||||
const handleButtonClick = (action: string) => {
|
||||
setClicked(true);
|
||||
setStatus(confirmed ? 'approved' : 'denied');
|
||||
ConfirmToolRequest(toolConfirmationId, confirmed);
|
||||
setStatus(action);
|
||||
if (action === ALWAYS_ALLOW) {
|
||||
setActionDisplay('always allowed');
|
||||
} else if (action === ALLOW_ONCE) {
|
||||
setActionDisplay('allowed once');
|
||||
} else {
|
||||
setActionDisplay('denied');
|
||||
}
|
||||
ConfirmToolRequest(toolConfirmationId, action);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
function getExtensionName(toolName: string): string {
|
||||
const parts = toolName.split('__');
|
||||
return parts.length > 1 ? parts[0] : '';
|
||||
}
|
||||
|
||||
return isCancelledMessage ? (
|
||||
<div className="goose-message-content bg-bgSubtle rounded-2xl px-4 py-2 text-textStandard">
|
||||
Tool call confirmation is cancelled.
|
||||
@@ -27,9 +58,9 @@ export default function ToolConfirmation({
|
||||
Goose would like to call the above tool. Allow?
|
||||
</div>
|
||||
{clicked ? (
|
||||
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 flex gap-4 mt-1">
|
||||
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-2 pb-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{status === 'approved' && (
|
||||
{status === 'always_allow' && (
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -41,7 +72,19 @@ export default function ToolConfirmation({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{status === 'denied' && (
|
||||
{status === 'allow_once' && (
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{status === 'deny' && (
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -56,30 +99,48 @@ export default function ToolConfirmation({
|
||||
<span className="ml-2 text-textStandard">
|
||||
{isClicked
|
||||
? 'Tool confirmation is not available'
|
||||
: `${snakeToTitleCase(toolName.substring(toolName.lastIndexOf('__') + 2))} is ${status}`}
|
||||
: `${snakeToTitleCase(toolName.substring(toolName.lastIndexOf('__') + 2))} is ${actionDisplay}`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center cursor-pointer" onClick={() => setIsModalOpen(true)}>
|
||||
<span className="mr-1 text-textStandard">Change</span>
|
||||
<ChevronRight className="w-4 h-4 ml-1 text-iconStandard" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-4 pb-2 flex gap-4 mt-1">
|
||||
<div className="goose-message-tool bg-bgApp border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 pt-2 pb-2 flex gap-2 items-center">
|
||||
<button
|
||||
className={
|
||||
'bg-black text-white dark:bg-white dark:text-black rounded-full px-6 py-2 transition'
|
||||
}
|
||||
onClick={() => handleButtonClick(true)}
|
||||
onClick={() => handleButtonClick(ALWAYS_ALLOW)}
|
||||
>
|
||||
Allow tool
|
||||
Always Allow
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
'bg-bgProminent text-white dark:text-white rounded-full px-6 py-2 transition'
|
||||
}
|
||||
onClick={() => handleButtonClick(ALLOW_ONCE)}
|
||||
>
|
||||
Allow Once
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
'bg-white text-black dark:bg-black dark:text-white border border-gray-300 dark:border-gray-700 rounded-full px-6 py-2 transition'
|
||||
}
|
||||
onClick={() => handleButtonClick(false)}
|
||||
onClick={() => handleButtonClick(DENY)}
|
||||
>
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal for updating tool permission */}
|
||||
{isModalOpen && (
|
||||
<PermissionModal onClose={handleModalClose} extensionName={getExtensionName(toolName)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { ChevronDownIcon, SlidersHorizontal } from 'lucide-react';
|
||||
import { getTools, PermissionLevel, ToolInfo, upsertPermissions } from '../../../api';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
|
||||
function getFirstSentence(text: string): string {
|
||||
const match = text.match(/^([^.?!]+[.?!])/);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
interface PermissionModalProps {
|
||||
extensionName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PermissionModal({ extensionName, onClose }: PermissionModalProps) {
|
||||
const permissionOptions = [
|
||||
{ value: 'always_allow', label: 'Always allow' },
|
||||
{ value: 'ask_before', label: 'Ask before' },
|
||||
{ value: 'never_allow', label: 'Never allow' },
|
||||
] as { value: PermissionLevel; label: string }[];
|
||||
|
||||
const [tools, setTools] = useState<ToolInfo[]>([]);
|
||||
const [updatedPermissions, setUpdatedPermissions] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTools = async () => {
|
||||
try {
|
||||
const response = await getTools({ query: { extension_name: extensionName } });
|
||||
if (response.error) {
|
||||
console.error('Failed to get tools');
|
||||
} else {
|
||||
setTools(response.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching tools:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTools();
|
||||
}, [extensionName]);
|
||||
|
||||
const handleSettingChange = (toolName: string, newPermission: PermissionLevel) => {
|
||||
setUpdatedPermissions((prev) => ({
|
||||
...prev,
|
||||
[toolName]: newPermission,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
tool_permissions: Object.entries(updatedPermissions).map(([toolName, permission]) => ({
|
||||
tool_name: toolName,
|
||||
permission: permission as PermissionLevel,
|
||||
})),
|
||||
};
|
||||
|
||||
if (payload.tool_permissions.length === 0) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await upsertPermissions({
|
||||
body: payload,
|
||||
});
|
||||
if (response.error) {
|
||||
console.error('Failed to save permissions:', response.error);
|
||||
} else {
|
||||
console.log('Permissions updated successfully');
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error saving permissions:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const footerContent = (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-textProminent font-medium text-md"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog.Root open={true} onOpenChange={(open) => !open && onClose()}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/30 dark:bg-white/20 transition-colors animate-[fadein_200ms_ease-in_forwards]" />
|
||||
<Dialog.Content className="fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-[500px] max-w-[90vw] bg-bgApp rounded-xl shadow-xl max-h-[90vh] flex flex-col">
|
||||
<div className="p-6">
|
||||
<Dialog.Title className="DialogTitle flex justify-start items-center mb-6">
|
||||
<SlidersHorizontal className="text-iconStandard" size={24} />
|
||||
<p className="ml-2 text-2xl font-semibold text-textStandard">{extensionName}</p>
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="DialogDescription"></Dialog.Description>
|
||||
|
||||
<div className="flex justify-center items-center h-full">
|
||||
{tools.length === 0 ? (
|
||||
<div className="flex items-center justify-center">
|
||||
{/* Loading spinner */}
|
||||
<svg
|
||||
className="animate-spin h-8 w-8 text-grey-50 dark:text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v8H4z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{tools.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
className="mb-4 flex items-center justify-between grid grid-cols-12"
|
||||
>
|
||||
<div className="flex flex-col col-span-8">
|
||||
<label className="block text-sm font-medium text-textStandard">
|
||||
{tool.name}
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
{getFirstSentence(tool.description)}
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger className="flex col-span-4 items-center justify-center bg-bgSubtle text-textStandard rounded-full px-3 py-2">
|
||||
<span>
|
||||
{permissionOptions.find(
|
||||
(option) =>
|
||||
option.value === (updatedPermissions[tool.name] || tool.permission)
|
||||
)?.label || 'Ask Before'}
|
||||
</span>
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="bg-white dark:bg-bgProminent rounded-lg shadow-md">
|
||||
{permissionOptions.map((option) => (
|
||||
<DropdownMenu.Item
|
||||
key={option.value}
|
||||
className="px-8 py-2 rounded-lg hover:cursor-pointer hover:bg-bgSubtle text-textStandard"
|
||||
onSelect={() =>
|
||||
handleSettingChange(tool.name, option.value as PermissionLevel)
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{footerContent && (
|
||||
<div className="border-t border-borderSubtle bg-bgApp w-full rounded-b-xl overflow-hidden">
|
||||
{footerContent}
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getApiUrl, getSecretKey } from '../config';
|
||||
|
||||
export async function ConfirmToolRequest(requesyId: string, confirmed: boolean) {
|
||||
export async function ConfirmToolRequest(requesyId: string, action: string) {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('/confirm'), {
|
||||
method: 'POST',
|
||||
@@ -10,7 +10,7 @@ export async function ConfirmToolRequest(requesyId: string, confirmed: boolean)
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: requesyId,
|
||||
confirmed,
|
||||
action,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user