feat: add permission field to the list tools response (#2080)

This commit is contained in:
Yingjie He
2025-04-08 09:47:29 -07:00
committed by GitHub
parent 76e2686c74
commit 318f419861
9 changed files with 152 additions and 17 deletions
+5 -2
View File
@@ -1,11 +1,12 @@
use utoipa::OpenApi;
use goose::agents::extension::Envs;
use goose::agents::extension::ToolInfo;
use goose::agents::ExtensionConfig;
use goose::config::permission::PermissionLevel;
use goose::config::ExtensionEntry;
use goose::providers::base::ConfigKey;
use goose::providers::base::ProviderMetadata;
use mcp_core::tool::{Tool, ToolAnnotations};
use utoipa::OpenApi;
#[allow(dead_code)] // Used by utoipa for OpenAPI generation
#[derive(OpenApi)]
@@ -36,6 +37,8 @@ use mcp_core::tool::{Tool, ToolAnnotations};
Envs,
Tool,
ToolAnnotations,
ToolInfo,
PermissionLevel,
))
)]
pub struct ApiDoc;
+38 -8
View File
@@ -1,13 +1,15 @@
use crate::state::AppState;
use axum::{
extract::State,
extract::{Query, State},
http::{HeaderMap, StatusCode},
routing::{get, post},
Json, Router,
};
use goose::config::Config;
use goose::{agents::AgentFactory, model::ModelConfig, providers};
use mcp_core::Tool;
use goose::{agents::AgentFactory, config::PermissionManager, model::ModelConfig, providers};
use goose::{
agents::{capabilities::get_parameter_names, extension::ToolInfo},
config::Config,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
@@ -62,6 +64,11 @@ struct ProviderList {
details: ProviderDetails,
}
#[derive(Deserialize)]
pub struct GetToolsQuery {
extension_name: Option<String>,
}
async fn get_versions() -> Json<VersionsResponse> {
let versions = AgentFactory::available_versions();
let default_version = AgentFactory::default_version().to_string();
@@ -167,6 +174,9 @@ async fn list_providers() -> Json<Vec<ProviderList>> {
#[utoipa::path(
get,
path = "/agent/tools",
params(
("extension_name" = Option<String>, Query, description = "Optional extension name to filter tools")
),
responses(
(status = 200, description = "Tools retrieved successfully", body = Vec<Tool>),
(status = 401, description = "Unauthorized - invalid secret key"),
@@ -177,7 +187,8 @@ async fn list_providers() -> Json<Vec<ProviderList>> {
async fn get_tools(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Vec<Tool>>, StatusCode> {
Query(query): Query<GetToolsQuery>,
) -> Result<Json<Vec<ToolInfo>>, StatusCode> {
let secret_key = headers
.get("X-Secret-Key")
.and_then(|value| value.to_str().ok())
@@ -189,11 +200,30 @@ async fn get_tools(
let mut agent = state.agent.write().await;
let agent = agent.as_mut().ok_or(StatusCode::PRECONDITION_REQUIRED)?;
let permission_manager = PermissionManager::default();
// Since list_tools() now returns Vec<Tool> directly, not a Result
let tools = agent.list_tools().await;
let tools = agent
.list_tools()
.await
.into_iter()
.filter(|tool| {
// Apply the filter only if the extension name is present in the query
if let Some(extension_name) = &query.extension_name {
tool.name.starts_with(extension_name)
} else {
true
}
})
.map(|tool| {
ToolInfo::new(
&tool.name,
&tool.description,
get_parameter_names(&tool),
permission_manager.get_user_permission(&tool.name),
)
})
.collect();
// Return the tools directly
Ok(Json(tools))
}
+10 -2
View File
@@ -9,6 +9,7 @@ use utoipa::ToSchema;
use crate::config;
use crate::config::extensions::name_to_key;
use crate::config::permission::PermissionLevel;
/// Errors from Extension operation
#[derive(Error, Debug)]
@@ -277,19 +278,26 @@ impl ExtensionInfo {
}
/// Information about the tool used for building prompts
#[derive(Clone, Debug, Serialize)]
#[derive(Clone, Debug, Serialize, ToSchema)]
pub struct ToolInfo {
name: String,
description: String,
parameters: Vec<String>,
permission: Option<PermissionLevel>,
}
impl ToolInfo {
pub fn new(name: &str, description: &str, parameters: Vec<String>) -> Self {
pub fn new(
name: &str,
description: &str,
parameters: Vec<String>,
permission: Option<PermissionLevel>,
) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
parameters,
permission,
}
}
}
+8 -1
View File
@@ -289,7 +289,14 @@ impl Agent for ReferenceAgent {
let tools = capabilities.get_prefixed_tools().await?;
let tools_info = tools
.into_iter()
.map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool)))
.map(|tool| {
ToolInfo::new(
&tool.name,
&tool.description,
get_parameter_names(&tool),
None,
)
})
.collect();
let plan_prompt = capabilities.get_planning_prompt(tools_info).await;
+8 -1
View File
@@ -489,7 +489,14 @@ impl Agent for SummarizeAgent {
let tools = capabilities.get_prefixed_tools().await?;
let tools_info = tools
.into_iter()
.map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool)))
.map(|tool| {
ToolInfo::new(
&tool.name,
&tool.description,
get_parameter_names(&tool),
None,
)
})
.collect();
let plan_prompt = capabilities.get_planning_prompt(tools_info).await;
+8 -1
View File
@@ -774,7 +774,14 @@ impl Agent for TruncateAgent {
let tools = capabilities.get_prefixed_tools().await?;
let tools_info = tools
.into_iter()
.map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool)))
.map(|tool| {
ToolInfo::new(
&tool.name,
&tool.description,
get_parameter_names(&tool),
None,
)
})
.collect();
let plan_prompt = capabilities.get_planning_prompt(tools_info).await;
+2 -1
View File
@@ -5,9 +5,10 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use utoipa::ToSchema;
/// Enum representing the possible permission levels for a tool.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PermissionLevel {
AlwaysAllow, // Tool can always be used without prompt
+52
View File
@@ -19,6 +19,18 @@
"super::routes::agent"
],
"operationId": "get_tools",
"parameters": [
{
"name": "extension_name",
"in": "query",
"description": "Optional extension name to filter tools",
"required": false,
"schema": {
"type": "string",
"nullable": true
}
}
],
"responses": {
"200": {
"description": "Tools retrieved successfully",
@@ -547,6 +559,15 @@
}
}
},
"PermissionLevel": {
"type": "string",
"description": "Enum representing the possible permission levels for a tool.",
"enum": [
"always_allow",
"ask_before",
"never_allow"
]
},
"ProviderDetails": {
"type": "object",
"required": [
@@ -688,6 +709,37 @@
}
}
},
"ToolInfo": {
"type": "object",
"description": "Information about the tool used for building prompts",
"required": [
"name",
"description",
"parameters"
],
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
},
"parameters": {
"type": "array",
"items": {
"type": "string"
}
},
"permission": {
"allOf": [
{
"$ref": "#/components/schemas/PermissionLevel"
}
],
"nullable": true
}
}
},
"UpsertConfigQuery": {
"type": "object",
"required": [
+21 -1
View File
@@ -84,6 +84,11 @@ export type ExtensionResponse = {
extensions: Array<ExtensionEntry>;
};
/**
* Enum representing the possible permission levels for a tool.
*/
export type PermissionLevel = 'always_allow' | 'ask_before' | 'never_allow';
export type ProviderDetails = {
/**
* Indicates whether the provider is fully configured
@@ -204,6 +209,16 @@ export type ToolAnnotations = {
title?: string | null;
};
/**
* Information about the tool used for building prompts
*/
export type ToolInfo = {
description: string;
name: string;
parameters: Array<string>;
permission?: PermissionLevel | null;
};
export type UpsertConfigQuery = {
is_secret: boolean;
key: string;
@@ -213,7 +228,12 @@ export type UpsertConfigQuery = {
export type GetToolsData = {
body?: never;
path?: never;
query?: never;
query?: {
/**
* Optional extension name to filter tools
*/
extension_name?: string | null;
};
url: '/agent/tools';
};