feat(plugins): add AI tools and consent-gated data access
This commit is contained in:
71 files changed
+4613
-129
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user