feat(plugins): add AI tools and consent-gated data access

This commit is contained in:
t8y2 committed 2026-09-25 01:56:26 +08:00
1 parent 1bc6f70cd8
commit 26a015cd86
71 files changed
+4613 -129

No files matched your search

+5
View File
@@ -55,6 +55,8 @@ const BLOCKED_EXACT: &[&str] = &[
"app-settings/config/decrypt",
// 读取服务器本地 ~/.ssh/config
"ssh/config-hosts",
// 插件数据访问授权写入(插件读取已存连接数据的许可)
"plugin/data/grant",
];
/// 非 GET 请求整族拦截的前缀:插件/JDBC/驱动安装与运行时控制、云同步外发通道。
@@ -76,6 +78,9 @@ const BLOCKED_AI_EXACT: &[&str] = &[
"ai/test-connection",
"ai/models",
"ai/model-effort",
// 允许内置 AI 调用插件工具的开关;预览会拉起插件 sidecar
"ai/plugin-tools/plugins",
"ai/plugin-tools/preview",
];
pub(crate) fn is_demo_blocked(method: &Method, suffix: &str) -> bool {
+9
View File
@@ -733,6 +733,9 @@ async fn serve() {
.route("/query/get-explain-info", post(routes::query::get_explain_info))
.route("/query/plugin-plan-capabilities", post(routes::query::get_plugin_plan_capabilities))
.route("/query/plugin-estimated-plan", post(routes::query::get_plugin_estimated_plan))
.route("/plugin/data/query", post(routes::query::query_plugin_data))
.route("/plugin/data/grants", post(routes::query::get_plugin_data_grants))
.route("/plugin/data/grant", post(routes::query::set_plugin_data_grant))
.route("/query/build-create-user-sql", post(routes::query::build_create_user_sql))
.route("/query/build-table-select-sql", post(routes::query::build_table_select_sql))
.route("/query/build-database-search-sql", post(routes::query::build_database_search_sql))
@@ -1211,6 +1214,12 @@ async fn serve() {
.route("/ai/stream", post(routes::ai::ai_stream))
.route("/ai/agent-stream", post(routes::ai::ai_agent_stream))
.route("/ai/cancel-stream", post(routes::ai::ai_cancel_stream))
.route("/ai/tool-approval", post(routes::ai::ai_resolve_tool_approval))
.route(
"/ai/plugin-tools/plugins",
get(routes::ai::get_ai_plugin_tool_plugins).post(routes::ai::set_ai_plugin_tool_plugin_enabled),
)
.route("/ai/plugin-tools/preview", post(routes::ai::preview_plugin_ai_tools))
.route("/ai/test-connection", post(routes::ai::ai_test_connection))
.route("/ai/models", post(routes::ai::ai_list_models))
.route("/ai/model-effort", post(routes::ai::ai_resolve_model_effort))
+56
View File
@@ -344,6 +344,58 @@ pub async fn ai_cancel_stream(Json(body): Json<AiCancelStreamRequest>) -> Result
Ok(Json(result))
}
// ---------------------------------------------------------------------------
// Plugin tools for the built-in agent
// ---------------------------------------------------------------------------
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AiResolveToolApprovalRequest {
pub session_id: String,
pub approval_id: String,
pub approved: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AiPluginToolPluginRequest {
pub plugin_id: String,
#[serde(default)]
pub enabled: bool,
}
/// Answers a pending plugin tool approval of the agent run `session_id`.
pub async fn ai_resolve_tool_approval(Json(body): Json<AiResolveToolApprovalRequest>) -> Result<Json<bool>, AppError> {
Ok(Json(dbx_core::tool_approval::resolve_tool_approval(&body.session_id, &body.approval_id, body.approved)))
}
pub async fn get_ai_plugin_tool_plugins(State(state): State<Arc<WebState>>) -> Result<Json<Vec<String>>, AppError> {
Ok(Json(state.app.storage.load_ai_plugin_tool_plugin_ids().await.map_err(AppError::from)?))
}
pub async fn set_ai_plugin_tool_plugin_enabled(
State(state): State<Arc<WebState>>,
Json(body): Json<AiPluginToolPluginRequest>,
) -> Result<Json<Vec<String>>, AppError> {
let plugin_ids = state
.app
.storage
.set_ai_plugin_tool_plugin_enabled(&body.plugin_id, body.enabled)
.await
.map_err(AppError::bad_request)?;
Ok(Json(plugin_ids))
}
pub async fn preview_plugin_ai_tools(
State(state): State<Arc<WebState>>,
Json(body): Json<AiPluginToolPluginRequest>,
) -> Result<Json<dbx_core::plugin_tools::PluginToolPreview>, AppError> {
let preview = dbx_core::plugin_tools::preview_plugin_tools(&state.app, None, &body.plugin_id)
.await
.map_err(AppError::bad_request)?;
Ok(Json(preview))
}
// ---------------------------------------------------------------------------
// AI stream (POST returns SSE directly)
// ---------------------------------------------------------------------------
@@ -461,6 +513,10 @@ pub async fn ai_agent_stream(
sql_permissions,
max_agent_turns,
prompt_cache_key: request.prompt_cache_key.clone(),
session_id: Some(session_id.clone()),
// The loop below runs on its own current-thread runtime; plugin
// sidecar calls must stay on the server runtime that owns the sessions.
host_runtime: Some(tokio::runtime::Handle::current()),
};
let sid = session_id.clone();
+5
View File
@@ -359,12 +359,17 @@ pub async fn uninstall_plugin(
}
let root_dir = state.app.plugins.root_dir().to_path_buf();
let app_version = state.app.plugins.app_version().to_string();
let plugin_id = request.plugin_id.clone();
tokio::task::spawn_blocking(move || {
PluginPackageInstaller::new(root_dir, app_version)?.uninstall(&request.plugin_id)
})
.await
.map_err(|error| AppError::internal(error.to_string()))?
.map_err(AppError::bad_request)?;
// A reinstall must ask for AI tool access and data grants again.
if let Err(error) = state.app.storage.forget_plugin_permissions(&plugin_id).await {
log::warn!("Failed to clear permissions of uninstalled plugin {plugin_id}: {error}");
}
list_plugins(State(state)).await
}
+53
View File
@@ -886,6 +886,59 @@ pub async fn get_plugin_estimated_plan(
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueryPluginDataRequest {
pub plugin_id: String,
pub request: dbx_core::query::plugin_data::PluginDataQueryRequest,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginDataGrantRequest {
pub plugin_id: String,
#[serde(default)]
pub connection_id: String,
#[serde(default)]
pub granted: bool,
}
/// Plugin Host API: consent-gated read-only data query (`host.data:read`).
pub async fn query_plugin_data(
State(state): State<Arc<WebState>>,
Json(body): Json<QueryPluginDataRequest>,
) -> Result<Json<dbx_core::query::plugin_data::PluginDataQueryResult>, AppError> {
let result = dbx_core::query::plugin_data::query_plugin_data(&state.app, &body.plugin_id, body.request)
.await
.map_err(AppError::from)?;
Ok(Json(result))
}
pub async fn get_plugin_data_grants(
State(state): State<Arc<WebState>>,
Json(body): Json<PluginDataGrantRequest>,
) -> Result<Json<Vec<dbx_core::query::plugin_data::PluginDataGrant>>, AppError> {
let grants = dbx_core::query::plugin_data::list_plugin_data_grants(&state.app, &body.plugin_id)
.await
.map_err(AppError::from)?;
Ok(Json(grants))
}
pub async fn set_plugin_data_grant(
State(state): State<Arc<WebState>>,
Json(body): Json<PluginDataGrantRequest>,
) -> Result<Json<Vec<dbx_core::query::plugin_data::PluginDataGrant>>, AppError> {
let grants = dbx_core::query::plugin_data::set_plugin_data_grant(
&state.app,
&body.plugin_id,
&body.connection_id,
body.granted,
)
.await
.map_err(AppError::bad_request)?;
Ok(Json(grants))
}
pub async fn build_create_user_sql(Json(req): Json<BuildCreateUserSqlRequest>) -> Result<Json<String>, AppError> {
Ok(Json(dbx_core::db_admin_sql::build_create_user_sql(&req.username, &req.password, &req.tablespace)))
}