Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,7 @@ Check each affected surface. Documentation-only changes need source, link, and i
- Theme preview tiles scope the same light/dark tokens instead of maintaining a second miniature palette.
- Automated contrast checks cover text on workspace, chrome, raised, selection, and semantic feedback surfaces, plus foregrounds on solid actions. Editor theme tests cover light/dark switching and token inheritance.
- Continue to verify real content, narrow windows, keyboard focus, system appearance changes, and native window controls on the target operating systems. Theme tests do not replace native-platform interaction checks.

### AI permission controls

Connection dialogs expose a collapsed AI permissions section shared by Data Studio and external MCP. Default to system inheritance, show effective allowed actions, and reveal per-action Inherit / Allow / Deny under Customize. The system settings page edits action defaults, without a separate connection allowlist or MCP exposure switch. Preserve shared Select, Label, Button, neutral surfaces, localized English/Chinese copy, keyboard focus, and loading/error/retry states. See [AI permissions](docs/AI_PERMISSIONS.md) for enforcement and migration behavior.
25 changes: 25 additions & 0 deletions docs/AI_PERMISSIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# AI permissions

SQL database and document database connections share one AI permission chain across built-in Data Studio and external MCP clients. This policy does not grant database privileges or change manual database access.

For each action (read, create data, update data, delete data, manage structure/administration), resolve an explicit connection allow/deny first, otherwise use the system default, otherwise deny. Omitted connection decisions inherit; explicit false is a denial. Presets are shortcuts for these individual decisions.

Data Studio only operates on attached connections. Inherited attachment permissions use the live connection policy, rather than a permission snapshot. Custom conversation restrictions can narrow access, but cannot grant an operation denied by the connection chain. Ask/Auto controls confirmation and never grants CRUD permissions. Backend checks run again before execution, including retries.

An empty set of allowed actions denies access. There is no separate MCP exposure setting or connection allowlist. Connection metadata returned to AI clients excludes inaccessible connections; Data Studio discovery is additionally limited to its attached connections. The MCP server's availability and auto-start preference remain service settings.

Read & modify data includes data deletion but excludes structure and administration. Mixed operations require every affected permission: SQL writes inspect statement tokens, bulk requests inspect every action, and aggregation pipelines that write require write and management access. Risk controls confirmation independently from action permissions.

Legacy allowlists migrate to explicit connection decisions preserving exclusions. When an allowlist existed, new connections default to no access. Existing per-connection restrictions are preserved. Corrupt or unreadable policy files deny execution rather than falling back to permissive settings. Policy writes are atomic and take effect for subsequent calls without restarting the bridge.

Both applications pin the shared agent runtime to an exact Git revision in their Cargo manifests and lockfiles. Each application owns its permission policy and database-operation classification.

The application-owned `ai_permissions` module resolves policy, migrates legacy settings, classifies database payloads, and gates both MCP and built-in agent execution. The shared agent routes requests through an application preparation hook and carries its confirmation risk and context; it does not resolve this permission chain. Execution rechecks the application policy on every attempt.

AI schema reads use the same application gate as tool calls. Cached schema and editor context are included only when the associated connections permit reading. Manual database UI commands retain their existing access behavior. SQLKit does not expose a generic capability invocation command to the WebView.

Agent executors are bound to a conversation and its connection/source identities. Before dispatch they reload connection policy, current attachment restrictions, and confirmation rules. Detaching or deleting a source prevents subsequent dispatches. The sidebar uses its bound active connection without requiring a Data Studio attachment.

Preparation rejects denied calls before displaying confirmation. A queued call whose confirmation requirements become stricter returns to the existing approval flow. Permission denials are terminal and do not trigger transport retries; approval never bypasses a later denial. Rechecks occur at dispatch and do not cancel operations already dispatched or erase context already sent to a model.

The host-hook API is introduced by [data-studio-agent #35](https://github.com/geek-fun/data-studio-agent/pull/35). Merge that runtime implementation before merging its consumers. Keep the exact compatible revision until a release containing these hooks exists; after the runtime is merged and released, update both the manifest and lockfile to that release. Older release tags do not provide this API.
17 changes: 4 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@types/dagre": "^0.7.54",
"@vueuse/core": "^14.1.0",
"dagre": "^0.8.5",
"dompurify": "^3.4.10",
"dompurify": "^3.4.17",
"highlight.js": "^11.11.1",
"json-with-bigint": "^3.5.8",
"json5": "^2.2.3",
Expand Down Expand Up @@ -132,5 +132,8 @@
"unocss-preset-shadcn": "^1.0.1",
"vite": "^6.0.3",
"vue-tsc": "^2.1.10"
},
"overrides": {
"dompurify": "$dompurify"
}
}
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ http = "1"
log = "0.4"
futures = "0.3"
rand = "0.8"
# data-studio-agent: pinned to latest release tag.
data-studio-agent = { git = "https://github.com/geek-fun/data-studio-agent.git", tag = "v0.1.9" }
# Pin the shared agent runtime to a reproducible revision.
# Requires the host hooks from data-studio-agent #35; older release tags are incompatible.
data-studio-agent = { git = "https://github.com/geek-fun/data-studio-agent.git", rev = "dc58625cdc721b64ef46a27bd6dcf1257fdd6f7b" }

# Archive extraction (JRE downloads)
flate2 = "1.0"
Expand Down
45 changes: 32 additions & 13 deletions src-tauri/src/agent/executor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use async_trait::async_trait;
use data_studio_agent::tool_executor::{ToolEnvelope, ToolExecutor, ToolResultMetadata};
use data_studio_agent::tool_executor::{
ToolCallPreparation, ToolEnvelope, ToolExecutionError, ToolExecutor, ToolResultMetadata,
};
use serde_json::Value;
use std::collections::HashMap;

const TOOL_ENVELOPE_MAX_CHARS: usize = 32768;
const TOOL_ENVELOPE_SUMMARY_CHARS: usize = 4096;
Expand All @@ -13,31 +16,47 @@ fn char_truncate(input: &str, max_chars: usize) -> (String, bool) {
(truncated, true)
}

pub struct SqlKitToolExecutor;
pub(crate) struct SqlKitToolExecutor {
pub(crate) access: crate::ai_permissions::AgentAccess,
}

#[async_trait]
impl ToolExecutor for SqlKitToolExecutor {
fn prepare(
&self,
tool_name: &str,
arguments: &Value,
config: &Value,
_connections: &HashMap<String, Value>,
) -> Result<ToolCallPreparation, String> {
crate::ai_permissions::prepare_agent_call(&self.access, tool_name, arguments, config)
}

async fn execute(
&self,
tool_name: &str,
arguments: &Value,
connection_config: &Value,
) -> Result<ToolEnvelope, String> {
let start = std::time::Instant::now();

let conn_opt = if connection_config.is_null() {
None
} else {
Some(connection_config.clone())
};
self.execute_checked(tool_name, arguments, connection_config)
.await
.map_err(|e| e.to_string())
}

let raw = data_studio_agent::capabilities::registry::invoke_capability_inner(
async fn execute_checked(
&self,
tool_name: &str,
arguments: &Value,
connection_config: &Value,
) -> Result<ToolEnvelope, ToolExecutionError> {
let start = std::time::Instant::now();
let raw = crate::ai_permissions::invoke_ai(
tool_name,
arguments.clone(),
conn_opt,
arguments,
connection_config,
Some(&self.access),
)
.await?;

let duration_ms = start.elapsed().as_millis() as u64;
let (full_result, truncated) = char_truncate(&raw, TOOL_ENVELOPE_MAX_CHARS);
let (summary, _) = char_truncate(&full_result, TOOL_ENVELOPE_SUMMARY_CHARS);
Expand Down
49 changes: 18 additions & 31 deletions src-tauri/src/agent_adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
//! to the shared library.

use std::collections::HashMap;
use std::sync::Arc;

use crate::mcp_bridge::McpConfig;
use data_studio_agent as lib;
use data_studio_agent::storage;
use data_studio_agent::traits::{CancelMap, ConfirmMap, EventEmitter};
Expand Down Expand Up @@ -49,9 +47,6 @@ pub async fn run_agent_loop(
let confirm_map: ConfirmMap = confirm_state.inner().clone();
let cancel_state: State<CancelMap> = app.state::<CancelMap>();
let cancel_map: CancelMap = cancel_state.inner().clone();
let executor_state: State<Arc<dyn lib::ToolExecutor>> =
app.state::<Arc<dyn lib::ToolExecutor>>();
let executor: Arc<dyn lib::ToolExecutor> = executor_state.inner().clone();

let connections: HashMap<String, Value> = settings
.get("connections")
Expand All @@ -62,51 +57,43 @@ pub async fn run_agent_loop(
.cloned()
.unwrap_or(Value::Null);

let studio = match settings.get("contextKind").and_then(Value::as_str) {
Some("dataStudio") => true,
Some("sidebarAssistant") => false,
_ => return Err("Unknown AI context".into()),
};
let executor = crate::agent::executor::SqlKitToolExecutor {
access: crate::ai_permissions::AgentAccess {
session_id: session_id.clone(),
connections: connections.clone(),
studio,
db: db_state.inner().clone(),
},
};

let is_parallel_ok = |name: &str| -> bool {
data_studio_agent::capabilities::registry::registry()
.get(name)
.map(|c| c.parallel_ok)
.unwrap_or(false)
};

let app_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
let should_deny = move |tool_name: &str, conn_id: Option<&str>| -> bool {
// Reload per call so permission changes made in Settings apply to the
// next tool call instead of being snapshotted for the whole run.
let policy = McpConfig::load(&app_data_dir).policy;
match data_studio_agent::capabilities::registry::registry().get(tool_name) {
Some(cap) => {
// Allowlist set but no connection specified: a DB capability would
// fall back to the default connection, bypassing the allowlist.
// AppLocal tools (list_connections etc.) need no connection.
if !policy.allowed_connection_ids.is_empty()
&& conn_id.is_none()
&& !matches!(
cap.source_kind,
data_studio_agent::capabilities::types::SourceKind::AppLocal
)
{
return true;
}
!policy.allows(cap.risk_level, conn_id)
}
None => false,
}
};
// Required by the shared loop API; authorization is owned by the executor at both stages.
let legacy_deny_hook = |_: &str, _: Option<&str>| false;

lib::loop_runner::run_agent_loop(
&session_id,
&user_message,
&settings,
&store,
&emitter,
executor.as_ref(),
&executor,
connections,
fallback,
&confirm_map,
&cancel_map,
&is_parallel_ok,
&should_deny,
&legacy_deny_hook,
)
.await
}
Expand Down
Loading
Loading