release: v0.92.0

This commit is contained in:
coso
2026-03-20 23:03:38 +08:00
parent 276c7eb9d5
commit 7242707293
188 changed files with 43591 additions and 13993 deletions
+1
View File
@@ -101,6 +101,7 @@ let stream = agent.reply(user_message, session_config, Some(cancel_token)).await
| `agent_runtime_submit_turn` | 统一提交 turn |
| `agent_runtime_interrupt_turn` | 统一中断 turn |
| `agent_runtime_create/list/get/update/delete_session` | 统一会话管理 |
| `agent_runtime_spawn/send_input/wait/resume/close_subagent` | subagent 控制面 |
| `agent_runtime_respond_action` | 统一响应工具确认 / ask / elicitation |
## 凭证池桥接
+4 -2
View File
@@ -24,8 +24,10 @@ pub use credential_bridge::{
create_aster_provider, AsterProviderConfig, CredentialBridge, CredentialBridgeError,
};
pub use lime_agent::{
convert_agent_event, convert_to_tauri_message, initialize_aster_runtime, QueuedTurnSnapshot,
QueuedTurnTask, TauriAgentEvent,
convert_agent_event, convert_to_tauri_message, initialize_aster_runtime,
ChildSubagentRuntimeStatus, ChildSubagentSession, QueuedTurnSnapshot, QueuedTurnTask,
SubagentControlState, SubagentParentContext, SubagentRuntimeStatus, SubagentRuntimeStatusKind,
TauriAgentEvent,
};
pub use subagent_scheduler::{
LimeScheduler, LimeSubAgentExecutor, SubAgentProgressEvent, SubAgentRole,
@@ -14,6 +14,7 @@ use aster::session::QueuedTurnRuntime;
use lime_agent::{
clear_runtime_queue as clear_runtime_queue_impl,
list_runtime_queue_snapshots as list_runtime_queue_snapshots_impl,
promote_runtime_queued_turn as promote_runtime_queued_turn_impl,
remove_runtime_queued_turn as remove_runtime_queued_turn_impl,
resume_persisted_runtime_queues_on_startup as resume_persisted_runtime_queues_on_startup_impl,
resume_runtime_queue_if_needed as resume_runtime_queue_if_needed_impl,
@@ -178,6 +179,13 @@ pub(crate) async fn remove_runtime_queued_turn(
.await
}
pub(crate) async fn promote_runtime_queued_turn(
session_id: &str,
queued_turn_id: &str,
) -> Result<bool, String> {
promote_runtime_queued_turn_impl(session_id, queued_turn_id).await
}
pub(crate) async fn resume_persisted_runtime_queues_on_startup(
app: AppHandle,
state: &AsterAgentState,
+805
View File
@@ -0,0 +1,805 @@
use crate::mcp::McpToolDefinition;
use serde::{Deserialize, Serialize};
pub const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
pub const SOCIAL_IMAGE_TOOL_NAME: &str = "social_generate_cover_image";
pub const LIME_CREATE_VIDEO_TASK_TOOL_NAME: &str = "lime_create_video_generation_task";
pub const LIME_CREATE_BROADCAST_TASK_TOOL_NAME: &str = "lime_create_broadcast_generation_task";
pub const LIME_CREATE_COVER_TASK_TOOL_NAME: &str = "lime_create_cover_generation_task";
pub const LIME_CREATE_RESOURCE_SEARCH_TASK_TOOL_NAME: &str =
"lime_create_modal_resource_search_task";
pub const LIME_CREATE_IMAGE_TASK_TOOL_NAME: &str = "lime_create_image_generation_task";
pub const LIME_CREATE_URL_PARSE_TASK_TOOL_NAME: &str = "lime_create_url_parse_task";
pub const LIME_CREATE_TYPESETTING_TASK_TOOL_NAME: &str = "lime_create_typesetting_task";
pub const BROWSER_RUNTIME_TOOL_PREFIX: &str = "mcp__lime-browser__";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolSurfaceProfile {
Core,
Creator,
BrowserAssist,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCapability {
Planning,
Delegation,
WebSearch,
SkillExecution,
SessionControl,
ContentCreation,
BrowserRuntime,
WorkspaceIo,
Execution,
Vision,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolLifecycle {
Current,
Compat,
Deprecated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolSourceKind {
AsterBuiltin,
LimeInjected,
BrowserCompatibility,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolPermissionPlane {
SessionAllowlist,
ParameterRestricted,
CallerFiltered,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct ToolCatalogEntry {
pub name: &'static str,
pub profiles: &'static [ToolSurfaceProfile],
pub capabilities: &'static [ToolCapability],
pub lifecycle: ToolLifecycle,
pub source: ToolSourceKind,
pub permission_plane: ToolPermissionPlane,
pub workspace_default_allow: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WorkspaceToolSurface {
pub creator: bool,
pub browser_assist: bool,
}
impl WorkspaceToolSurface {
pub const fn core() -> Self {
Self {
creator: false,
browser_assist: false,
}
}
pub const fn creator() -> Self {
Self {
creator: true,
browser_assist: false,
}
}
pub const fn browser_assist() -> Self {
Self {
creator: false,
browser_assist: true,
}
}
pub const fn creator_with_browser_assist() -> Self {
Self {
creator: true,
browser_assist: true,
}
}
pub const fn includes_profile(self, profile: ToolSurfaceProfile) -> bool {
match profile {
ToolSurfaceProfile::Core => true,
ToolSurfaceProfile::Creator => self.creator,
ToolSurfaceProfile::BrowserAssist => self.browser_assist,
}
}
}
const CORE_PROFILES: &[ToolSurfaceProfile] = &[ToolSurfaceProfile::Core];
const CREATOR_PROFILES: &[ToolSurfaceProfile] = &[ToolSurfaceProfile::Creator];
const BROWSER_PROFILES: &[ToolSurfaceProfile] = &[ToolSurfaceProfile::BrowserAssist];
const PLAN_CAP: &[ToolCapability] = &[ToolCapability::Planning];
const DELEGATION_CAP: &[ToolCapability] =
&[ToolCapability::Delegation, ToolCapability::SessionControl];
const SEARCH_CAP: &[ToolCapability] = &[ToolCapability::WebSearch];
const SKILL_CAP: &[ToolCapability] = &[ToolCapability::SkillExecution];
const CONTENT_CAP: &[ToolCapability] = &[ToolCapability::ContentCreation];
const BROWSER_CAP: &[ToolCapability] = &[ToolCapability::BrowserRuntime];
const WORKSPACE_IO_CAP: &[ToolCapability] = &[ToolCapability::WorkspaceIo];
const EXECUTION_CAP: &[ToolCapability] = &[ToolCapability::Execution];
const VISION_CAP: &[ToolCapability] = &[ToolCapability::Vision];
static NATIVE_TOOL_CATALOG: &[ToolCatalogEntry] = &[
ToolCatalogEntry {
name: "read",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "write",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "edit",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "glob",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "grep",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "bash",
profiles: CORE_PROFILES,
capabilities: EXECUTION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "lsp",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "Skill",
profiles: CORE_PROFILES,
capabilities: SKILL_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "Task",
profiles: CORE_PROFILES,
capabilities: EXECUTION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "TaskOutput",
profiles: CORE_PROFILES,
capabilities: PLAN_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "KillShell",
profiles: CORE_PROFILES,
capabilities: EXECUTION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "TodoWrite",
profiles: CORE_PROFILES,
capabilities: PLAN_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "NotebookEdit",
profiles: CORE_PROFILES,
capabilities: WORKSPACE_IO_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "EnterPlanMode",
profiles: CORE_PROFILES,
capabilities: PLAN_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "ExitPlanMode",
profiles: CORE_PROFILES,
capabilities: PLAN_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "WebFetch",
profiles: CORE_PROFILES,
capabilities: SEARCH_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "WebSearch",
profiles: CORE_PROFILES,
capabilities: SEARCH_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "analyze_image",
profiles: CORE_PROFILES,
capabilities: VISION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::ParameterRestricted,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: "ask",
profiles: CORE_PROFILES,
capabilities: PLAN_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::AsterBuiltin,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: TOOL_SEARCH_TOOL_NAME,
profiles: CORE_PROFILES,
capabilities: SEARCH_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "spawn_agent",
profiles: CORE_PROFILES,
capabilities: DELEGATION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "send_input",
profiles: CORE_PROFILES,
capabilities: DELEGATION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "wait_agent",
profiles: CORE_PROFILES,
capabilities: DELEGATION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "resume_agent",
profiles: CORE_PROFILES,
capabilities: DELEGATION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "close_agent",
profiles: CORE_PROFILES,
capabilities: DELEGATION_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: "SubAgentTask",
profiles: CORE_PROFILES,
capabilities: DELEGATION_CAP,
lifecycle: ToolLifecycle::Compat,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: false,
},
ToolCatalogEntry {
name: SOCIAL_IMAGE_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_VIDEO_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_BROADCAST_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_COVER_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_RESOURCE_SEARCH_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_IMAGE_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_URL_PARSE_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: LIME_CREATE_TYPESETTING_TASK_TOOL_NAME,
profiles: CREATOR_PROFILES,
capabilities: CONTENT_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::LimeInjected,
permission_plane: ToolPermissionPlane::SessionAllowlist,
workspace_default_allow: true,
},
ToolCatalogEntry {
name: BROWSER_RUNTIME_TOOL_PREFIX,
profiles: BROWSER_PROFILES,
capabilities: BROWSER_CAP,
lifecycle: ToolLifecycle::Current,
source: ToolSourceKind::BrowserCompatibility,
permission_plane: ToolPermissionPlane::CallerFiltered,
workspace_default_allow: false,
},
];
pub fn native_tool_catalog() -> &'static [ToolCatalogEntry] {
NATIVE_TOOL_CATALOG
}
pub fn tool_catalog_entry(tool_name: &str) -> Option<&'static ToolCatalogEntry> {
let normalized_name = tool_name.trim();
native_tool_catalog()
.iter()
.filter(|entry| {
if entry.name.ends_with("__") {
normalized_name.starts_with(entry.name)
} else {
entry.name == normalized_name
}
})
.max_by_key(|entry| entry.name.len())
}
pub fn tool_catalog_entries_for_surface(
surface: WorkspaceToolSurface,
) -> Vec<&'static ToolCatalogEntry> {
native_tool_catalog()
.iter()
.filter(|entry| {
entry
.profiles
.iter()
.any(|profile| surface.includes_profile(*profile))
})
.collect()
}
pub fn workspace_default_allowed_tool_names(surface: WorkspaceToolSurface) -> Vec<&'static str> {
let mut names = tool_catalog_entries_for_surface(surface)
.into_iter()
.filter(|entry| entry.workspace_default_allow)
.filter(|entry| entry.lifecycle == ToolLifecycle::Current)
.filter(|entry| !entry.name.ends_with("__"))
.map(|entry| entry.name)
.collect::<Vec<_>>();
names.sort_unstable();
names.dedup();
names
}
pub fn workspace_allowed_tool_names(surface: WorkspaceToolSurface) -> Vec<&'static str> {
workspace_default_allowed_tool_names(surface)
}
pub fn creator_tool_names() -> Vec<&'static str> {
tool_catalog_entries_for_surface(WorkspaceToolSurface::creator())
.into_iter()
.filter(|entry| entry.profiles.contains(&ToolSurfaceProfile::Creator))
.filter(|entry| entry.name != BROWSER_RUNTIME_TOOL_PREFIX)
.map(|entry| entry.name)
.collect()
}
pub fn browser_runtime_tool_prefix() -> &'static str {
BROWSER_RUNTIME_TOOL_PREFIX
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpExtensionSurface {
pub extension_name: String,
pub description: String,
pub available_tools: Vec<String>,
pub always_expose_tools: Vec<String>,
pub deferred_loading: bool,
pub allowed_caller: Option<String>,
}
impl McpExtensionSurface {
pub fn has_tools(&self) -> bool {
!self.available_tools.is_empty()
}
}
pub fn build_mcp_extension_surface(
extension_name: &str,
description: impl Into<String>,
tools: &[McpToolDefinition],
) -> McpExtensionSurface {
let mut available_tools = tools
.iter()
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
available_tools.sort();
available_tools.dedup();
let mut always_expose_tools = tools
.iter()
.filter(|tool| {
tool.always_visible.unwrap_or(false) || !tool.deferred_loading.unwrap_or(false)
})
.map(|tool| tool.name.clone())
.collect::<Vec<_>>();
always_expose_tools.sort();
always_expose_tools.dedup();
let deferred_loading = tools
.iter()
.any(|tool| tool.deferred_loading.unwrap_or(false));
let allowed_caller = collapse_extension_allowed_caller(tools);
McpExtensionSurface {
extension_name: extension_name.to_string(),
description: description.into(),
available_tools,
always_expose_tools,
deferred_loading,
allowed_caller,
}
}
fn collapse_extension_allowed_caller(tools: &[McpToolDefinition]) -> Option<String> {
let mut collapsed: Option<String> = None;
for tool in tools {
let allowed = tool.allowed_callers.as_ref()?;
if allowed.len() != 1 {
return None;
}
let caller = allowed[0].trim();
if caller.is_empty() {
return None;
}
match collapsed.as_deref() {
Some(existing) if existing != caller => return None,
Some(_) => {}
None => collapsed = Some(caller.to_string()),
}
}
collapsed
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn sample_tool(
name: &str,
deferred_loading: Option<bool>,
always_visible: Option<bool>,
allowed_callers: Option<Vec<&str>>,
) -> McpToolDefinition {
McpToolDefinition {
name: name.to_string(),
description: format!("desc for {name}"),
input_schema: serde_json::json!({ "type": "object" }),
server_name: "docs".to_string(),
deferred_loading,
always_visible,
allowed_callers: allowed_callers.map(|items| {
items
.into_iter()
.map(|item| item.to_string())
.collect::<Vec<_>>()
}),
input_examples: None,
tags: None,
}
}
#[test]
fn test_tool_catalog_entry_matches_browser_prefix() {
let entry = tool_catalog_entry("mcp__lime-browser__navigate")
.expect("browser tool should match prefix catalog entry");
assert_eq!(entry.name, BROWSER_RUNTIME_TOOL_PREFIX);
assert_eq!(entry.source, ToolSourceKind::BrowserCompatibility);
}
#[test]
fn test_workspace_default_allowed_tool_names_excludes_parameter_restricted_tools() {
let names = workspace_default_allowed_tool_names(WorkspaceToolSurface::core());
assert!(names.contains(&"spawn_agent"));
assert!(names.contains(&"WebSearch"));
assert!(!names.contains(&"SubAgentTask"));
assert!(!names.contains(&"read"));
assert!(!names.contains(&"bash"));
assert!(!names.contains(&SOCIAL_IMAGE_TOOL_NAME));
}
#[test]
fn test_workspace_default_allowed_tool_names_includes_creator_surface() {
let names = workspace_default_allowed_tool_names(WorkspaceToolSurface::creator());
assert!(names.contains(&SOCIAL_IMAGE_TOOL_NAME));
assert!(names.contains(&LIME_CREATE_VIDEO_TASK_TOOL_NAME));
}
#[test]
fn test_tool_catalog_entries_for_surface_counts_and_lifecycle_boundaries() {
let core = tool_catalog_entries_for_surface(WorkspaceToolSurface::core());
assert_eq!(core.len(), 26);
assert_eq!(
core.iter()
.filter(|entry| entry.lifecycle == ToolLifecycle::Current)
.count(),
25
);
assert_eq!(
core.iter()
.filter(|entry| entry.lifecycle == ToolLifecycle::Compat)
.count(),
1
);
assert!(core
.iter()
.all(|entry| !entry.profiles.contains(&ToolSurfaceProfile::Creator)));
assert!(core
.iter()
.all(|entry| !entry.profiles.contains(&ToolSurfaceProfile::BrowserAssist)));
let creator = tool_catalog_entries_for_surface(WorkspaceToolSurface::creator());
assert_eq!(creator.len(), 34);
assert!(creator
.iter()
.any(|entry| entry.name == SOCIAL_IMAGE_TOOL_NAME));
assert!(!creator
.iter()
.any(|entry| entry.name == BROWSER_RUNTIME_TOOL_PREFIX));
let browser = tool_catalog_entries_for_surface(WorkspaceToolSurface::browser_assist());
assert_eq!(browser.len(), 27);
assert!(browser
.iter()
.any(|entry| entry.name == BROWSER_RUNTIME_TOOL_PREFIX));
let combined =
tool_catalog_entries_for_surface(WorkspaceToolSurface::creator_with_browser_assist());
assert_eq!(combined.len(), 35);
}
#[test]
fn test_creator_tool_names_only_returns_creator_increment() {
let names = creator_tool_names().into_iter().collect::<BTreeSet<_>>();
assert_eq!(names.len(), 8);
assert!(names.contains(SOCIAL_IMAGE_TOOL_NAME));
assert!(names.contains(LIME_CREATE_VIDEO_TASK_TOOL_NAME));
assert!(!names.contains("tool_search"));
assert!(!names.contains(BROWSER_RUNTIME_TOOL_PREFIX));
}
#[test]
fn test_workspace_default_allowed_tool_names_creator_with_browser_assist_excludes_prefix_tool()
{
let names = workspace_default_allowed_tool_names(
WorkspaceToolSurface::creator_with_browser_assist(),
);
assert_eq!(names.len(), 22);
assert!(names.contains(&SOCIAL_IMAGE_TOOL_NAME));
assert!(names.contains(&"tool_search"));
assert!(!names
.iter()
.any(|name| name.starts_with(BROWSER_RUNTIME_TOOL_PREFIX)));
}
#[test]
fn test_build_mcp_extension_surface_collapses_single_caller() {
let tools = vec![
sample_tool(
"search_docs",
Some(true),
Some(false),
Some(vec!["assistant"]),
),
sample_tool(
"read_docs",
Some(false),
Some(true),
Some(vec!["assistant"]),
),
];
let surface = build_mcp_extension_surface("docs", "docs tools", &tools);
assert!(surface.deferred_loading);
assert_eq!(surface.allowed_caller.as_deref(), Some("assistant"));
assert_eq!(surface.always_expose_tools, vec!["read_docs".to_string()]);
}
#[test]
fn test_build_mcp_extension_surface_drops_mixed_callers() {
let tools = vec![
sample_tool(
"search_docs",
Some(true),
Some(false),
Some(vec!["assistant"]),
),
sample_tool(
"admin_docs",
Some(true),
Some(false),
Some(vec!["code_execution"]),
),
];
let surface = build_mcp_extension_surface("docs", "docs tools", &tools);
assert_eq!(surface.allowed_caller, None);
}
#[test]
fn test_build_mcp_extension_surface_dedups_available_and_exposed_tools() {
let tools = vec![
sample_tool(
"search_docs",
Some(true),
Some(true),
Some(vec!["assistant"]),
),
sample_tool(
"read_docs",
Some(false),
Some(false),
Some(vec!["assistant"]),
),
sample_tool(
"search_docs",
Some(true),
Some(true),
Some(vec!["assistant"]),
),
];
let surface = build_mcp_extension_surface("docs", "docs tools", &tools);
assert!(surface.deferred_loading);
assert_eq!(surface.allowed_caller.as_deref(), Some("assistant"));
assert_eq!(
surface.available_tools,
vec!["read_docs".to_string(), "search_docs".to_string()]
);
assert_eq!(
surface.always_expose_tools,
vec!["read_docs".to_string(), "search_docs".to_string()]
);
}
#[test]
fn test_build_mcp_extension_surface_rejects_blank_allowed_caller() {
let tools = vec![
sample_tool(
"search_docs",
Some(true),
Some(false),
Some(vec!["assistant"]),
),
sample_tool("read_docs", Some(false), Some(true), Some(vec![" "])),
];
let surface = build_mcp_extension_surface("docs", "docs tools", &tools);
assert_eq!(surface.allowed_caller, None);
}
}
+966
View File
@@ -0,0 +1,966 @@
use crate::agent_tools::catalog::{
tool_catalog_entries_for_surface, tool_catalog_entry, workspace_default_allowed_tool_names,
ToolPermissionPlane, WorkspaceToolSurface,
};
use aster::permission::{ParameterRestriction, PermissionScope, RestrictionType, ToolPermission};
use lime_core::config::{
ToolExecutionOverrideConfig as ConfigToolExecutionOverrideConfig,
ToolExecutionPolicyConfig as ConfigToolExecutionPolicyConfig,
ToolExecutionRestrictionProfileConfig as ConfigToolExecutionRestrictionProfileConfig,
ToolExecutionSandboxProfileConfig as ConfigToolExecutionSandboxProfileConfig,
ToolExecutionWarningPolicyConfig as ConfigToolExecutionWarningPolicyConfig,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map as JsonMap, Value as JsonValue};
use std::collections::HashMap;
const DURABLE_MEMORY_PATH_PATTERN: &str = r"^/memories(?:/.*)?$";
const SAFE_HTTPS_URL_PATTERN: &str = r"^https://[^\s]+$";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolExecutionWarningPolicy {
None,
ShellCommandRisk,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolExecutionRestrictionProfile {
None,
WorkspacePathRequired,
WorkspacePathOptional,
WorkspaceAbsolutePathRequired,
WorkspaceShellCommand,
AnalyzeImageInput,
SafeHttpsUrlRequired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolExecutionSandboxProfile {
None,
WorkspaceCommand,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolExecutionPolicySource {
Default,
Persisted,
Runtime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolExecutionPolicy {
pub warning_policy: ToolExecutionWarningPolicy,
pub restriction_profile: ToolExecutionRestrictionProfile,
pub sandbox_profile: ToolExecutionSandboxProfile,
}
impl Default for ToolExecutionPolicy {
fn default() -> Self {
Self {
warning_policy: ToolExecutionWarningPolicy::None,
restriction_profile: ToolExecutionRestrictionProfile::None,
sandbox_profile: ToolExecutionSandboxProfile::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolExecutionPolicyResolution {
pub policy: ToolExecutionPolicy,
pub warning_policy_source: ToolExecutionPolicySource,
pub restriction_profile_source: ToolExecutionPolicySource,
pub sandbox_profile_source: ToolExecutionPolicySource,
}
#[derive(Debug, Clone, Copy)]
pub struct WorkspaceExecutionPermissionInput<'a> {
pub surface: WorkspaceToolSurface,
pub workspace_root: &'a str,
pub auto_mode: bool,
pub execution_policy_input: ToolExecutionResolverInput<'a>,
}
#[derive(Debug, Clone)]
struct WorkspacePermissionPatterns {
workspace_path_pattern: String,
workspace_abs_path_pattern: String,
analyze_image_path_pattern: String,
safe_https_url_pattern: String,
shell_allow_pattern: String,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ToolExecutionResolverInput<'a> {
pub persisted_policy: Option<&'a ConfigToolExecutionPolicyConfig>,
pub request_metadata: Option<&'a JsonValue>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct ToolExecutionPolicyOverride {
warning_policy: Option<ToolExecutionWarningPolicy>,
restriction_profile: Option<ToolExecutionRestrictionProfile>,
sandbox_profile: Option<ToolExecutionSandboxProfile>,
}
pub fn tool_execution_policy(tool_name: &str) -> ToolExecutionPolicy {
let normalized_name = tool_name.trim();
let Some(catalog_entry) = tool_catalog_entry(normalized_name) else {
return ToolExecutionPolicy::default();
};
match catalog_entry.name {
"read" | "write" | "edit" | "lsp" => ToolExecutionPolicy {
restriction_profile: ToolExecutionRestrictionProfile::WorkspacePathRequired,
..ToolExecutionPolicy::default()
},
"glob" | "grep" => ToolExecutionPolicy {
restriction_profile: ToolExecutionRestrictionProfile::WorkspacePathOptional,
..ToolExecutionPolicy::default()
},
"bash" => ToolExecutionPolicy {
warning_policy: ToolExecutionWarningPolicy::ShellCommandRisk,
restriction_profile: ToolExecutionRestrictionProfile::WorkspaceShellCommand,
sandbox_profile: ToolExecutionSandboxProfile::WorkspaceCommand,
},
"Task" => ToolExecutionPolicy {
warning_policy: ToolExecutionWarningPolicy::ShellCommandRisk,
restriction_profile: ToolExecutionRestrictionProfile::WorkspaceShellCommand,
sandbox_profile: ToolExecutionSandboxProfile::None,
},
"NotebookEdit" => ToolExecutionPolicy {
restriction_profile: ToolExecutionRestrictionProfile::WorkspaceAbsolutePathRequired,
..ToolExecutionPolicy::default()
},
"analyze_image" => ToolExecutionPolicy {
restriction_profile: ToolExecutionRestrictionProfile::AnalyzeImageInput,
..ToolExecutionPolicy::default()
},
"WebFetch" => ToolExecutionPolicy {
restriction_profile: ToolExecutionRestrictionProfile::SafeHttpsUrlRequired,
..ToolExecutionPolicy::default()
},
_ => ToolExecutionPolicy::default(),
}
}
pub fn resolve_tool_execution_policy(
tool_name: &str,
input: ToolExecutionResolverInput<'_>,
) -> ToolExecutionPolicy {
resolve_tool_execution_policy_resolution(tool_name, input).policy
}
pub fn resolve_tool_execution_policy_resolution(
tool_name: &str,
input: ToolExecutionResolverInput<'_>,
) -> ToolExecutionPolicyResolution {
let default_policy = tool_execution_policy(tool_name);
let persisted_override =
extract_persisted_tool_execution_override(tool_name, input.persisted_policy);
let runtime_override =
extract_runtime_execution_policy_override(tool_name, input.request_metadata);
apply_tool_execution_override(
apply_tool_execution_override(
ToolExecutionPolicyResolution {
policy: default_policy,
warning_policy_source: ToolExecutionPolicySource::Default,
restriction_profile_source: ToolExecutionPolicySource::Default,
sandbox_profile_source: ToolExecutionPolicySource::Default,
},
persisted_override,
ToolExecutionPolicySource::Persisted,
),
runtime_override,
ToolExecutionPolicySource::Runtime,
)
}
pub fn build_workspace_shell_allow_pattern(
escaped_root: &str,
allow_extended_shell_commands: bool,
) -> String {
if allow_extended_shell_commands {
return String::from(r"(?s)^\s*\S.*$");
}
format!(
r"^\s*(?:cd\s+({escaped_root}|\.|\./|\.\./)|pwd|ls(?:\s+[^;&|]+)?|find\s+({escaped_root}|\.|\./|\.\./)[^;&|]*|rg\b[^;&|]*|grep\b[^;&|]*|cat\s+({escaped_root}|\.|\./|\.\./)[^;&|]*)\s*$"
)
}
pub fn should_auto_approve_tool_warnings(
tool_name: &str,
auto_mode: bool,
input: ToolExecutionResolverInput<'_>,
) -> bool {
auto_mode
&& matches!(
resolve_tool_execution_policy(tool_name, input).warning_policy,
ToolExecutionWarningPolicy::ShellCommandRisk
)
}
pub fn build_workspace_execution_permissions(
input: WorkspaceExecutionPermissionInput<'_>,
) -> Vec<ToolPermission> {
let patterns = build_workspace_permission_patterns(input.workspace_root, input.auto_mode);
let mut permissions = tool_catalog_entries_for_surface(input.surface)
.into_iter()
.filter_map(|entry| {
build_parameter_restricted_permission(
entry.name,
input.auto_mode,
&patterns,
input.execution_policy_input,
)
})
.collect::<Vec<_>>();
if input.auto_mode {
permissions.push(ToolPermission {
tool: "*".to_string(),
allowed: true,
priority: 1000,
conditions: Vec::new(),
parameter_restrictions: Vec::new(),
scope: PermissionScope::Session,
reason: Some("Auto 模式:允许所有工具与参数".to_string()),
expires_at: None,
metadata: HashMap::new(),
});
}
for tool_name in workspace_default_allowed_tool_names(input.surface) {
permissions.push(ToolPermission {
tool: tool_name.to_string(),
allowed: true,
priority: 88,
conditions: Vec::new(),
parameter_restrictions: Vec::new(),
scope: PermissionScope::Session,
reason: Some(format!("允许默认工具: {tool_name}")),
expires_at: None,
metadata: HashMap::new(),
});
}
permissions.push(ToolPermission {
tool: "*".to_string(),
allowed: false,
priority: 10,
conditions: Vec::new(),
parameter_restrictions: Vec::new(),
scope: PermissionScope::Session,
reason: Some("workspace 安全策略:未显式授权的工具默认拒绝".to_string()),
expires_at: None,
metadata: HashMap::new(),
});
permissions
}
fn extract_persisted_tool_execution_override(
tool_name: &str,
persisted_policy: Option<&ConfigToolExecutionPolicyConfig>,
) -> ToolExecutionPolicyOverride {
let Some(tool_override) = persisted_policy
.and_then(|policy| find_tool_override_config(&policy.tool_overrides, tool_name))
else {
return ToolExecutionPolicyOverride::default();
};
ToolExecutionPolicyOverride {
warning_policy: tool_override
.warning_policy
.map(convert_warning_policy_config),
restriction_profile: tool_override
.restriction_profile
.map(convert_restriction_profile_config),
sandbox_profile: tool_override
.sandbox_profile
.map(convert_sandbox_profile_config),
}
}
fn extract_runtime_execution_policy_override(
tool_name: &str,
request_metadata: Option<&JsonValue>,
) -> ToolExecutionPolicyOverride {
let Some(execution_policy) = extract_runtime_execution_policy_object(request_metadata) else {
return ToolExecutionPolicyOverride::default();
};
let tool_overrides = find_named_object(execution_policy, &["tool_overrides", "toolOverrides"])
.unwrap_or(execution_policy);
let Some(tool_override) = find_case_insensitive_object(tool_overrides, tool_name) else {
return ToolExecutionPolicyOverride::default();
};
ToolExecutionPolicyOverride {
warning_policy: extract_named_string(tool_override, &["warning_policy", "warningPolicy"])
.and_then(parse_warning_policy),
restriction_profile: extract_named_string(
tool_override,
&["restriction_profile", "restrictionProfile"],
)
.and_then(parse_restriction_profile),
sandbox_profile: extract_named_string(
tool_override,
&["sandbox_profile", "sandboxProfile"],
)
.and_then(parse_sandbox_profile),
}
}
fn extract_runtime_execution_policy_object(
request_metadata: Option<&JsonValue>,
) -> Option<&JsonMap<String, JsonValue>> {
let harness = extract_runtime_harness_object(request_metadata)?;
find_named_object(harness, &["execution_policy", "executionPolicy"])
}
fn extract_runtime_harness_object(
request_metadata: Option<&JsonValue>,
) -> Option<&JsonMap<String, JsonValue>> {
let metadata = request_metadata?.as_object()?;
metadata
.get("harness")
.and_then(JsonValue::as_object)
.or(Some(metadata))
}
fn find_named_object<'a>(
object: &'a JsonMap<String, JsonValue>,
keys: &[&str],
) -> Option<&'a JsonMap<String, JsonValue>> {
keys.iter()
.filter_map(|key| object.get(*key))
.find_map(JsonValue::as_object)
}
fn find_case_insensitive_object<'a>(
object: &'a JsonMap<String, JsonValue>,
key: &str,
) -> Option<&'a JsonMap<String, JsonValue>> {
let normalized_key = key.trim();
object
.get(normalized_key)
.and_then(JsonValue::as_object)
.or_else(|| {
object.iter().find_map(|(candidate, value)| {
candidate
.trim()
.eq_ignore_ascii_case(normalized_key)
.then_some(value)
.and_then(JsonValue::as_object)
})
})
}
fn find_tool_override_config<'a>(
tool_overrides: &'a HashMap<String, ConfigToolExecutionOverrideConfig>,
tool_name: &str,
) -> Option<&'a ConfigToolExecutionOverrideConfig> {
let normalized_name = tool_name.trim();
tool_overrides.get(normalized_name).or_else(|| {
tool_overrides
.iter()
.find_map(|(candidate, override_config)| {
candidate
.trim()
.eq_ignore_ascii_case(normalized_name)
.then_some(override_config)
})
})
}
fn extract_named_string<'a>(
object: &'a JsonMap<String, JsonValue>,
keys: &[&str],
) -> Option<&'a str> {
keys.iter()
.filter_map(|key| object.get(*key))
.find_map(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
}
fn apply_tool_execution_override(
mut base: ToolExecutionPolicyResolution,
tool_override: ToolExecutionPolicyOverride,
source: ToolExecutionPolicySource,
) -> ToolExecutionPolicyResolution {
if let Some(value) = tool_override.warning_policy {
base.policy.warning_policy = value;
base.warning_policy_source = source;
}
if let Some(value) = tool_override.restriction_profile {
base.policy.restriction_profile = value;
base.restriction_profile_source = source;
}
if let Some(value) = tool_override.sandbox_profile {
base.policy.sandbox_profile = value;
base.sandbox_profile_source = source;
}
base
}
fn convert_warning_policy_config(
value: ConfigToolExecutionWarningPolicyConfig,
) -> ToolExecutionWarningPolicy {
match value {
ConfigToolExecutionWarningPolicyConfig::None => ToolExecutionWarningPolicy::None,
ConfigToolExecutionWarningPolicyConfig::ShellCommandRisk => {
ToolExecutionWarningPolicy::ShellCommandRisk
}
}
}
fn convert_restriction_profile_config(
value: ConfigToolExecutionRestrictionProfileConfig,
) -> ToolExecutionRestrictionProfile {
match value {
ConfigToolExecutionRestrictionProfileConfig::None => ToolExecutionRestrictionProfile::None,
ConfigToolExecutionRestrictionProfileConfig::WorkspacePathRequired => {
ToolExecutionRestrictionProfile::WorkspacePathRequired
}
ConfigToolExecutionRestrictionProfileConfig::WorkspacePathOptional => {
ToolExecutionRestrictionProfile::WorkspacePathOptional
}
ConfigToolExecutionRestrictionProfileConfig::WorkspaceAbsolutePathRequired => {
ToolExecutionRestrictionProfile::WorkspaceAbsolutePathRequired
}
ConfigToolExecutionRestrictionProfileConfig::WorkspaceShellCommand => {
ToolExecutionRestrictionProfile::WorkspaceShellCommand
}
ConfigToolExecutionRestrictionProfileConfig::AnalyzeImageInput => {
ToolExecutionRestrictionProfile::AnalyzeImageInput
}
ConfigToolExecutionRestrictionProfileConfig::SafeHttpsUrlRequired => {
ToolExecutionRestrictionProfile::SafeHttpsUrlRequired
}
}
}
fn convert_sandbox_profile_config(
value: ConfigToolExecutionSandboxProfileConfig,
) -> ToolExecutionSandboxProfile {
match value {
ConfigToolExecutionSandboxProfileConfig::None => ToolExecutionSandboxProfile::None,
ConfigToolExecutionSandboxProfileConfig::WorkspaceCommand => {
ToolExecutionSandboxProfile::WorkspaceCommand
}
}
}
fn parse_warning_policy(value: &str) -> Option<ToolExecutionWarningPolicy> {
match value.trim() {
"none" => Some(ToolExecutionWarningPolicy::None),
"shell_command_risk" => Some(ToolExecutionWarningPolicy::ShellCommandRisk),
_ => None,
}
}
fn parse_restriction_profile(value: &str) -> Option<ToolExecutionRestrictionProfile> {
match value.trim() {
"none" => Some(ToolExecutionRestrictionProfile::None),
"workspace_path_required" => Some(ToolExecutionRestrictionProfile::WorkspacePathRequired),
"workspace_path_optional" => Some(ToolExecutionRestrictionProfile::WorkspacePathOptional),
"workspace_absolute_path_required" => {
Some(ToolExecutionRestrictionProfile::WorkspaceAbsolutePathRequired)
}
"workspace_shell_command" => Some(ToolExecutionRestrictionProfile::WorkspaceShellCommand),
"analyze_image_input" => Some(ToolExecutionRestrictionProfile::AnalyzeImageInput),
"safe_https_url_required" => Some(ToolExecutionRestrictionProfile::SafeHttpsUrlRequired),
_ => None,
}
}
fn parse_sandbox_profile(value: &str) -> Option<ToolExecutionSandboxProfile> {
match value.trim() {
"none" => Some(ToolExecutionSandboxProfile::None),
"workspace_command" => Some(ToolExecutionSandboxProfile::WorkspaceCommand),
_ => None,
}
}
fn build_workspace_permission_patterns(
workspace_root: &str,
auto_mode: bool,
) -> WorkspacePermissionPatterns {
let escaped_root = regex::escape(workspace_root.trim());
WorkspacePermissionPatterns {
workspace_path_pattern: format!(
r"^(?:({escaped_root}|\.|\./|\.\./).*$|{DURABLE_MEMORY_PATH_PATTERN})"
),
workspace_abs_path_pattern: format!(r"^({escaped_root}).*$"),
analyze_image_path_pattern: format!(
r"^(base64:[A-Za-z0-9+/=]+|file://({escaped_root}).*|({escaped_root}|\.|\./|\.\./).*)$"
),
safe_https_url_pattern: SAFE_HTTPS_URL_PATTERN.to_string(),
shell_allow_pattern: build_workspace_shell_allow_pattern(&escaped_root, auto_mode),
}
}
fn build_parameter_restricted_permission(
tool_name: &str,
auto_mode: bool,
patterns: &WorkspacePermissionPatterns,
execution_policy_input: ToolExecutionResolverInput<'_>,
) -> Option<ToolPermission> {
let catalog_entry = tool_catalog_entry(tool_name)?;
if catalog_entry.permission_plane != ToolPermissionPlane::ParameterRestricted {
return None;
}
let policy = resolve_tool_execution_policy(tool_name, execution_policy_input);
let parameter_restrictions = if auto_mode {
Vec::new()
} else {
build_parameter_restrictions(tool_name, policy.restriction_profile, patterns)
};
Some(ToolPermission {
tool: tool_name.to_string(),
allowed: true,
priority: permission_priority(tool_name),
conditions: Vec::new(),
parameter_restrictions,
scope: PermissionScope::Session,
reason: Some(permission_reason(
tool_name,
policy.restriction_profile,
auto_mode,
)),
expires_at: None,
metadata: HashMap::new(),
})
}
fn build_parameter_restrictions(
tool_name: &str,
profile: ToolExecutionRestrictionProfile,
patterns: &WorkspacePermissionPatterns,
) -> Vec<ParameterRestriction> {
match profile {
ToolExecutionRestrictionProfile::None => Vec::new(),
ToolExecutionRestrictionProfile::WorkspacePathRequired => {
vec![pattern_restriction(
"path",
&patterns.workspace_path_pattern,
true,
Some(format!(
"{tool_name}.path 必须在 workspace、相对路径或 `/memories/` 内"
)),
)]
}
ToolExecutionRestrictionProfile::WorkspacePathOptional => {
vec![pattern_restriction(
"path",
&patterns.workspace_path_pattern,
false,
Some(format!(
"{tool_name}.path 必须在 workspace、相对路径或 `/memories/` 内"
)),
)]
}
ToolExecutionRestrictionProfile::WorkspaceAbsolutePathRequired => {
vec![pattern_restriction(
"notebook_path",
&patterns.workspace_abs_path_pattern,
true,
Some("NotebookEdit.notebook_path 必须是 workspace 内绝对路径".to_string()),
)]
}
ToolExecutionRestrictionProfile::WorkspaceShellCommand => vec![
pattern_restriction(
"command",
&patterns.shell_allow_pattern,
false,
Some(format!("{tool_name}.command 仅允许 workspace 内安全命令")),
),
pattern_restriction(
"cmd",
&patterns.shell_allow_pattern,
false,
Some(format!("{tool_name}.cmd 兼容参数名,规则与 command 一致")),
),
],
ToolExecutionRestrictionProfile::AnalyzeImageInput => {
vec![pattern_restriction(
"file_path",
&patterns.analyze_image_path_pattern,
true,
Some(
"analyze_image.file_path 仅允许 base64、workspace 内绝对路径或相对路径"
.to_string(),
),
)]
}
ToolExecutionRestrictionProfile::SafeHttpsUrlRequired => {
vec![pattern_restriction(
"url",
&patterns.safe_https_url_pattern,
true,
Some("WebFetch.url 仅允许 https 且禁止内网/本机地址".to_string()),
)]
}
}
}
fn pattern_restriction(
parameter: &str,
pattern: &str,
required: bool,
description: Option<String>,
) -> ParameterRestriction {
ParameterRestriction {
parameter: parameter.to_string(),
restriction_type: RestrictionType::Pattern,
values: None,
pattern: Some(pattern.to_string()),
validator: None,
min: None,
max: None,
required,
description,
}
}
fn permission_priority(tool_name: &str) -> i32 {
match tool_name {
"read" | "write" | "edit" | "glob" | "grep" => 100,
"bash" => 90,
_ => 88,
}
}
fn permission_reason(
tool_name: &str,
profile: ToolExecutionRestrictionProfile,
auto_mode: bool,
) -> String {
if auto_mode {
return match profile {
ToolExecutionRestrictionProfile::WorkspaceShellCommand => {
format!("Auto 模式:允许 {tool_name} 执行任意命令")
}
ToolExecutionRestrictionProfile::SafeHttpsUrlRequired => {
format!("Auto 模式:允许 {tool_name} 访问任意 URL")
}
ToolExecutionRestrictionProfile::AnalyzeImageInput => {
format!("Auto 模式:允许 {tool_name} 分析任意图片路径或 base64")
}
ToolExecutionRestrictionProfile::WorkspaceAbsolutePathRequired => {
format!("Auto 模式:允许 {tool_name} 访问任意绝对路径")
}
ToolExecutionRestrictionProfile::WorkspacePathRequired
| ToolExecutionRestrictionProfile::WorkspacePathOptional => {
format!("Auto 模式:允许 {tool_name} 访问任意路径")
}
ToolExecutionRestrictionProfile::None => format!("Auto 模式:允许工具 {tool_name}"),
};
}
match profile {
ToolExecutionRestrictionProfile::WorkspacePathRequired => {
format!("仅允许 {tool_name} 访问当前 workspace 或 `/memories/` 内容")
}
ToolExecutionRestrictionProfile::WorkspacePathOptional => {
format!("仅允许 {tool_name} 在当前 workspace 或 `/memories/` 搜索内容")
}
ToolExecutionRestrictionProfile::WorkspaceAbsolutePathRequired => {
format!("仅允许 {tool_name} 访问 workspace 内绝对路径")
}
ToolExecutionRestrictionProfile::WorkspaceShellCommand => {
format!("workspace 安全策略:{tool_name} 仅允许 workspace 内安全命令")
}
ToolExecutionRestrictionProfile::AnalyzeImageInput => {
"允许分析 workspace 内图片或 base64 数据".to_string()
}
ToolExecutionRestrictionProfile::SafeHttpsUrlRequired => {
"允许安全的 WebFetch 请求".to_string()
}
ToolExecutionRestrictionProfile::None => format!("允许工具 {tool_name}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use lime_core::config::{
ToolExecutionOverrideConfig as ConfigToolExecutionOverrideConfig,
ToolExecutionPolicyConfig as ConfigToolExecutionPolicyConfig,
ToolExecutionRestrictionProfileConfig as ConfigToolExecutionRestrictionProfileConfig,
ToolExecutionSandboxProfileConfig as ConfigToolExecutionSandboxProfileConfig,
ToolExecutionWarningPolicyConfig as ConfigToolExecutionWarningPolicyConfig,
};
use serde_json::json;
#[test]
fn test_tool_execution_policy_marks_bash_as_sandboxed_shell_risk() {
let policy = tool_execution_policy("bash");
assert_eq!(
policy.warning_policy,
ToolExecutionWarningPolicy::ShellCommandRisk
);
assert_eq!(
policy.restriction_profile,
ToolExecutionRestrictionProfile::WorkspaceShellCommand
);
assert_eq!(
policy.sandbox_profile,
ToolExecutionSandboxProfile::WorkspaceCommand
);
}
#[test]
fn test_build_workspace_execution_permissions_strict_mode_restricts_parameter_tools() {
let permissions =
build_workspace_execution_permissions(WorkspaceExecutionPermissionInput {
surface: WorkspaceToolSurface::core(),
workspace_root: "/tmp/workspace",
auto_mode: false,
execution_policy_input: ToolExecutionResolverInput::default(),
});
let read = permissions
.iter()
.find(|permission| permission.tool == "read")
.expect("read permission should exist");
assert_eq!(read.parameter_restrictions.len(), 1);
assert_eq!(read.parameter_restrictions[0].parameter, "path");
assert!(read.parameter_restrictions[0]
.pattern
.as_deref()
.unwrap_or_default()
.contains("/tmp/workspace"));
let bash = permissions
.iter()
.find(|permission| permission.tool == "bash")
.expect("bash permission should exist");
assert_eq!(bash.parameter_restrictions.len(), 2);
assert!(permissions
.iter()
.any(|permission| permission.tool == "*" && !permission.allowed));
assert!(!permissions
.iter()
.any(|permission| permission.tool == "*" && permission.allowed));
}
#[test]
fn test_build_workspace_execution_permissions_auto_mode_adds_wildcard_allow() {
let permissions =
build_workspace_execution_permissions(WorkspaceExecutionPermissionInput {
surface: WorkspaceToolSurface::core(),
workspace_root: "/tmp/workspace",
auto_mode: true,
execution_policy_input: ToolExecutionResolverInput::default(),
});
let bash = permissions
.iter()
.find(|permission| permission.tool == "bash")
.expect("bash permission should exist");
assert!(bash.parameter_restrictions.is_empty());
assert!(permissions
.iter()
.any(|permission| permission.tool == "*" && permission.allowed));
}
#[test]
fn test_should_auto_approve_tool_warnings_only_for_shell_risk_tools() {
let input = ToolExecutionResolverInput::default();
assert!(should_auto_approve_tool_warnings("bash", true, input));
assert!(should_auto_approve_tool_warnings("Task", true, input));
assert!(!should_auto_approve_tool_warnings("read", true, input));
assert!(!should_auto_approve_tool_warnings("bash", false, input));
}
#[test]
fn test_build_workspace_shell_allow_pattern_auto_mode_allows_multiline() {
let escaped_root = regex::escape("/tmp/workspace");
let pattern = build_workspace_shell_allow_pattern(&escaped_root, true);
let regex = regex::Regex::new(&pattern).expect("pattern should compile");
assert!(regex.is_match("python3 <<'EOF'\nprint('hello')\nEOF"));
}
#[test]
fn test_resolve_tool_execution_policy_allows_persisted_override_to_replace_default() {
let persisted_policy = ConfigToolExecutionPolicyConfig {
tool_overrides: HashMap::from([(
"bash".to_string(),
ConfigToolExecutionOverrideConfig {
warning_policy: Some(ConfigToolExecutionWarningPolicyConfig::None),
restriction_profile: Some(
ConfigToolExecutionRestrictionProfileConfig::WorkspacePathRequired,
),
sandbox_profile: Some(ConfigToolExecutionSandboxProfileConfig::None),
},
)]),
};
let policy = resolve_tool_execution_policy(
"bash",
ToolExecutionResolverInput {
persisted_policy: Some(&persisted_policy),
request_metadata: None,
},
);
assert_eq!(policy.warning_policy, ToolExecutionWarningPolicy::None);
assert_eq!(
policy.restriction_profile,
ToolExecutionRestrictionProfile::WorkspacePathRequired
);
assert_eq!(policy.sandbox_profile, ToolExecutionSandboxProfile::None);
}
#[test]
fn test_resolve_tool_execution_policy_runtime_override_beats_persisted_policy() {
let persisted_policy = ConfigToolExecutionPolicyConfig {
tool_overrides: HashMap::from([(
"bash".to_string(),
ConfigToolExecutionOverrideConfig {
warning_policy: Some(ConfigToolExecutionWarningPolicyConfig::None),
restriction_profile: Some(
ConfigToolExecutionRestrictionProfileConfig::WorkspacePathRequired,
),
sandbox_profile: Some(ConfigToolExecutionSandboxProfileConfig::None),
},
)]),
};
let request_metadata = json!({
"harness": {
"executionPolicy": {
"toolOverrides": {
"BASH": {
"warningPolicy": "shell_command_risk",
"restrictionProfile": "workspace_shell_command",
"sandboxProfile": "workspace_command"
}
}
}
}
});
let policy = resolve_tool_execution_policy(
"bash",
ToolExecutionResolverInput {
persisted_policy: Some(&persisted_policy),
request_metadata: Some(&request_metadata),
},
);
assert_eq!(
policy.warning_policy,
ToolExecutionWarningPolicy::ShellCommandRisk
);
assert_eq!(
policy.restriction_profile,
ToolExecutionRestrictionProfile::WorkspaceShellCommand
);
assert_eq!(
policy.sandbox_profile,
ToolExecutionSandboxProfile::WorkspaceCommand
);
}
#[test]
fn test_resolve_tool_execution_policy_resolution_tracks_mixed_sources_per_field() {
let persisted_policy = ConfigToolExecutionPolicyConfig {
tool_overrides: HashMap::from([(
"bash".to_string(),
ConfigToolExecutionOverrideConfig {
warning_policy: Some(ConfigToolExecutionWarningPolicyConfig::None),
restriction_profile: None,
sandbox_profile: None,
},
)]),
};
let request_metadata = json!({
"harness": {
"executionPolicy": {
"toolOverrides": {
"bash": {
"sandboxProfile": "none"
}
}
}
}
});
let resolution = resolve_tool_execution_policy_resolution(
"bash",
ToolExecutionResolverInput {
persisted_policy: Some(&persisted_policy),
request_metadata: Some(&request_metadata),
},
);
assert_eq!(
resolution.policy.warning_policy,
ToolExecutionWarningPolicy::None
);
assert_eq!(
resolution.policy.restriction_profile,
ToolExecutionRestrictionProfile::WorkspaceShellCommand
);
assert_eq!(
resolution.policy.sandbox_profile,
ToolExecutionSandboxProfile::None
);
assert_eq!(
resolution.warning_policy_source,
ToolExecutionPolicySource::Persisted
);
assert_eq!(
resolution.restriction_profile_source,
ToolExecutionPolicySource::Default
);
assert_eq!(
resolution.sandbox_profile_source,
ToolExecutionPolicySource::Runtime
);
}
#[test]
fn test_build_workspace_execution_permissions_respects_runtime_override() {
let request_metadata = json!({
"harness": {
"execution_policy": {
"tool_overrides": {
"bash": {
"restriction_profile": "none"
}
}
}
}
});
let permissions =
build_workspace_execution_permissions(WorkspaceExecutionPermissionInput {
surface: WorkspaceToolSurface::core(),
workspace_root: "/tmp/workspace",
auto_mode: false,
execution_policy_input: ToolExecutionResolverInput {
persisted_policy: None,
request_metadata: Some(&request_metadata),
},
});
let bash = permissions
.iter()
.find(|permission| permission.tool == "bash")
.expect("bash permission should exist");
assert!(bash.parameter_restrictions.is_empty());
}
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
pub mod catalog;
pub mod execution;
pub mod inventory;
+19 -11
View File
@@ -191,6 +191,8 @@ pub fn run() {
.manage(automation_service_state)
.manage(workflow_service)
.manage(progress_store)
.manage(commands::subagent_cmd::SubAgentSchedulerState::default())
.manage(commands::websocket_cmd::WsServiceState::default())
.manage(lime_gateway::telegram::TelegramGatewayState::default())
.manage(lime_gateway::discord::DiscordGatewayState::default())
.manage(lime_gateway::feishu::FeishuGatewayState::default())
@@ -242,14 +244,6 @@ pub fn run() {
crate::commands::windows_startup_cmd::maybe_show_windows_startup_notice(&app.handle());
}
// TODO: 重新实现 TerminalTool 和 TermScrollbackTool 的 AppHandle 设置
// 当前暂时注释掉,等待适配 aster-rust 工具系统
// crate::agent::tools::set_terminal_tool_app_handle(app.handle().clone());
// tracing::info!("[启动] TerminalTool AppHandle 已设置");
// crate::agent::tools::set_term_scrollback_tool_app_handle(app.handle().clone());
// tracing::info!("[启动] TermScrollbackTool AppHandle 已设置");
// 初始化托盘管理器
// Requirements 1.4: 应用启动时显示停止状态图标
match TrayManager::new(app.handle()) {
@@ -1345,6 +1339,8 @@ pub fn run() {
commands::plugin_install_cmd::is_plugin_installed,
// Plugin UI commands
commands::plugin_cmd::get_plugins_with_ui,
commands::plugin_cmd::get_plugin_ui,
commands::plugin_cmd::handle_plugin_action,
commands::plugin_cmd::read_plugin_manifest_cmd,
commands::plugin_cmd::launch_plugin_ui,
commands::plugin_cmd::frontend_debug_log,
@@ -1387,9 +1383,6 @@ pub fn run() {
commands::agent_cmd::agent_stop_process,
commands::agent_cmd::agent_get_process_status,
commands::agent_cmd::agent_generate_title,
// TODO: 重新启用这些命令,适配 aster-rust 工具系统
// commands::agent_cmd::agent_terminal_command_response,
// commands::agent_cmd::agent_term_scrollback_response,
// Aster Agent commands
commands::aster_agent_cmd::aster_agent_init,
commands::aster_agent_cmd::aster_agent_status,
@@ -1398,10 +1391,17 @@ pub fn run() {
commands::aster_agent_cmd::aster_agent_configure_from_pool,
commands::aster_agent_cmd::agent_runtime_submit_turn,
commands::aster_agent_cmd::agent_runtime_interrupt_turn,
commands::aster_agent_cmd::agent_runtime_promote_queued_turn,
commands::aster_agent_cmd::agent_runtime_remove_queued_turn,
commands::aster_agent_cmd::agent_runtime_create_session,
commands::aster_agent_cmd::agent_runtime_list_sessions,
commands::aster_agent_cmd::agent_runtime_get_session,
commands::aster_agent_cmd::agent_runtime_get_tool_inventory,
commands::aster_agent_cmd::agent_runtime_spawn_subagent,
commands::aster_agent_cmd::agent_runtime_send_subagent_input,
commands::aster_agent_cmd::agent_runtime_wait_subagents,
commands::aster_agent_cmd::agent_runtime_resume_subagent,
commands::aster_agent_cmd::agent_runtime_close_subagent,
commands::aster_agent_cmd::agent_runtime_update_session,
commands::aster_agent_cmd::agent_runtime_delete_session,
commands::aster_agent_cmd::agent_runtime_respond_action,
@@ -1478,6 +1478,10 @@ pub fn run() {
commands::terminal_cmd::terminal_close,
commands::terminal_cmd::terminal_list_sessions,
commands::terminal_cmd::terminal_get_session,
// SubAgent commands
commands::subagent_cmd::init_subagent_scheduler,
commands::subagent_cmd::execute_subagent_tasks,
commands::subagent_cmd::cancel_subagent_tasks,
// Connection commands
commands::connection_cmd::connection_list,
commands::connection_cmd::connection_add,
@@ -1489,6 +1493,10 @@ pub fn run() {
commands::connection_cmd::connection_save_raw_config,
commands::connection_cmd::connection_test,
commands::connection_cmd::connection_import_ssh_host,
// WebSocket commands
commands::websocket_cmd::get_websocket_status,
commands::websocket_cmd::get_websocket_connections,
commands::websocket_cmd::set_websocket_enabled,
// Browser environment preset commands
commands::browser_environment_cmd::list_browser_environment_presets_cmd,
commands::browser_environment_cmd::save_browser_environment_preset_cmd,
File diff suppressed because it is too large Load Diff
+64 -10
View File
@@ -10,7 +10,7 @@ pub mod dispatcher;
#[cfg(debug_assertions)]
use axum::{
extract::State,
http::{HeaderValue, Method},
http::{request::Parts as RequestParts, HeaderValue, Method},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
@@ -22,7 +22,7 @@ use std::sync::Arc;
#[cfg(debug_assertions)]
use tokio::sync::RwLock;
#[cfg(debug_assertions)]
use tower_http::cors::CorsLayer;
use tower_http::cors::{AllowOrigin, CorsLayer};
#[cfg(debug_assertions)]
use crate::{app, database::DbConnection};
@@ -89,6 +89,23 @@ impl Default for DevBridgeConfig {
#[cfg(debug_assertions)]
pub struct DevBridgeServer;
#[cfg(debug_assertions)]
fn is_allowed_loopback_origin(origin: &HeaderValue, _request_parts: &RequestParts) -> bool {
let Ok(origin) = origin.to_str() else {
return false;
};
let Ok(parsed) = url::Url::parse(origin) else {
return false;
};
matches!(parsed.scheme(), "http" | "https")
&& matches!(
parsed.host_str(),
Some("localhost") | Some("127.0.0.1") | Some("[::1]") | Some("::1")
)
}
#[cfg(debug_assertions)]
impl DevBridgeServer {
/// 启动开发桥接服务器
@@ -124,20 +141,13 @@ impl DevBridgeServer {
shared_stats,
};
let allowed_origins = vec![
HeaderValue::from_static("http://localhost:1420"),
HeaderValue::from_static("http://127.0.0.1:1420"),
HeaderValue::from_static("http://localhost:5173"),
HeaderValue::from_static("http://127.0.0.1:5173"),
];
let app = Router::new()
.route("/invoke", post(invoke_command))
.route("/health", get(health_check).post(health_check))
.layer(
// CORS 配置 - 允许本地开发前端访问
CorsLayer::new()
.allow_origin(allowed_origins)
.allow_origin(AllowOrigin::predicate(is_allowed_loopback_origin))
.allow_methods([Method::POST, Method::GET, Method::OPTIONS])
.allow_headers([axum::http::header::CONTENT_TYPE]),
)
@@ -196,3 +206,47 @@ async fn health_check() -> impl IntoResponse {
"version": "1.0.0"
}))
}
#[cfg(all(test, debug_assertions))]
mod tests {
use super::is_allowed_loopback_origin;
use axum::http::{request::Parts as RequestParts, HeaderValue, Request};
fn empty_parts() -> RequestParts {
let request = Request::builder().uri("/invoke").body(()).unwrap();
let (parts, _) = request.into_parts();
parts
}
#[test]
fn allows_loopback_dev_origins_with_any_port() {
let parts = empty_parts();
assert!(is_allowed_loopback_origin(
&HeaderValue::from_static("http://127.0.0.1:1421"),
&parts,
));
assert!(is_allowed_loopback_origin(
&HeaderValue::from_static("http://localhost:5173"),
&parts,
));
assert!(is_allowed_loopback_origin(
&HeaderValue::from_static("https://localhost:3000"),
&parts,
));
}
#[test]
fn rejects_non_loopback_origins() {
let parts = empty_parts();
assert!(!is_allowed_loopback_origin(
&HeaderValue::from_static("https://example.com"),
&parts,
));
assert!(!is_allowed_loopback_origin(
&HeaderValue::from_static("http://192.168.1.10:1420"),
&parts,
));
}
}
+10
View File
@@ -11,10 +11,12 @@ mod memory;
mod memory_runtime;
mod models;
mod openclaw;
mod plugins;
mod project_resources;
mod providers;
mod runtime_queries;
mod skills;
mod tray;
mod workspace;
use crate::dev_bridge::DevBridgeState;
@@ -115,10 +117,18 @@ pub async fn handle_command(
return Ok(result);
}
if let Some(result) = plugins::try_handle(state, cmd, args.as_ref()).await? {
return Ok(result);
}
if let Some(result) = agent_sessions::try_handle(state, cmd, args.as_ref()).await? {
return Ok(result);
}
if let Some(result) = tray::try_handle(state, cmd, args.as_ref()).await? {
return Ok(result);
}
if let Some(result) = workspace::try_handle(state, cmd, args.as_ref())? {
return Ok(result);
}
@@ -1,12 +1,229 @@
use super::{args_or_default, get_string_arg, parse_nested_arg, require_app_handle};
use crate::dev_bridge::DevBridgeState;
use serde::de::DeserializeOwned;
use serde_json::Value as JsonValue;
use tauri::Manager;
type DynError = Box<dyn std::error::Error>;
fn parse_request<T: DeserializeOwned>(args: Option<&JsonValue>) -> Result<T, DynError> {
parse_nested_arg(&args_or_default(args), "request")
}
pub(super) async fn try_handle(
_state: &DevBridgeState,
_cmd: &str,
_args: Option<&JsonValue>,
state: &DevBridgeState,
cmd: &str,
args: Option<&JsonValue>,
) -> Result<Option<JsonValue>, DynError> {
Ok(None)
if !matches!(
cmd,
"agent_runtime_submit_turn"
| "agent_runtime_interrupt_turn"
| "agent_runtime_create_session"
| "agent_runtime_list_sessions"
| "agent_runtime_get_session"
| "agent_runtime_update_session"
| "agent_runtime_delete_session"
| "agent_runtime_promote_queued_turn"
| "agent_runtime_remove_queued_turn"
| "agent_runtime_respond_action"
) {
return Ok(None);
}
let app_handle = require_app_handle(state)?;
let result = match cmd {
"agent_runtime_submit_turn" => {
let request = parse_request::<
crate::commands::aster_agent_cmd::AgentRuntimeSubmitTurnRequest,
>(args)?;
let aster_state = app_handle.state::<crate::agent::AsterAgentState>();
let db = app_handle.state::<crate::database::DbConnection>();
let api_key_provider_service =
app_handle
.state::<crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState>();
let logs = app_handle.state::<crate::app::LogState>();
let config_manager = app_handle.state::<crate::config::GlobalConfigManagerState>();
let mcp_manager = app_handle.state::<crate::mcp::McpManagerState>();
let automation_state =
app_handle.state::<crate::services::automation_service::AutomationServiceState>();
crate::commands::aster_agent_cmd::agent_runtime_submit_turn(
app_handle.clone(),
aster_state,
db,
api_key_provider_service,
logs,
config_manager,
mcp_manager,
automation_state,
request,
)
.await?;
JsonValue::Null
}
"agent_runtime_interrupt_turn" => {
let request = parse_request::<
crate::commands::aster_agent_cmd::AgentRuntimeInterruptTurnRequest,
>(args)?;
let aster_state = app_handle.state::<crate::agent::AsterAgentState>();
serde_json::to_value(
crate::commands::aster_agent_cmd::agent_runtime_interrupt_turn(
app_handle.clone(),
aster_state,
request,
)
.await?,
)?
}
"agent_runtime_create_session" => {
let args = args_or_default(args);
let workspace_id = get_string_arg(&args, "workspaceId", "workspace_id")?;
let name = args
.get("name")
.and_then(|value| value.as_str())
.map(ToString::to_string);
let execution_strategy = args
.get("executionStrategy")
.or_else(|| args.get("execution_strategy"))
.cloned()
.map(
serde_json::from_value::<
crate::commands::aster_agent_cmd::AsterExecutionStrategy,
>,
)
.transpose()?;
let db = app_handle.state::<crate::database::DbConnection>();
serde_json::to_value(
crate::commands::aster_agent_cmd::agent_runtime_create_session(
db,
workspace_id,
name,
execution_strategy,
)
.await?,
)?
}
"agent_runtime_list_sessions" => {
let db = app_handle.state::<crate::database::DbConnection>();
let logs = app_handle.state::<crate::app::LogState>();
serde_json::to_value(
crate::commands::aster_agent_cmd::agent_runtime_list_sessions(db, logs).await?,
)?
}
"agent_runtime_get_session" => {
let args = args_or_default(args);
let session_id = get_string_arg(&args, "sessionId", "session_id")?;
let aster_state = app_handle.state::<crate::agent::AsterAgentState>();
let db = app_handle.state::<crate::database::DbConnection>();
let api_key_provider_service =
app_handle
.state::<crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState>();
let logs = app_handle.state::<crate::app::LogState>();
let config_manager = app_handle.state::<crate::config::GlobalConfigManagerState>();
let mcp_manager = app_handle.state::<crate::mcp::McpManagerState>();
let automation_state =
app_handle.state::<crate::services::automation_service::AutomationServiceState>();
serde_json::to_value(
crate::commands::aster_agent_cmd::agent_runtime_get_session(
app_handle.clone(),
aster_state,
db,
api_key_provider_service,
logs,
config_manager,
mcp_manager,
automation_state,
session_id,
)
.await?,
)?
}
"agent_runtime_update_session" => {
let request = parse_request::<
crate::commands::aster_agent_cmd::AgentRuntimeUpdateSessionRequest,
>(args)?;
let db = app_handle.state::<crate::database::DbConnection>();
crate::commands::aster_agent_cmd::agent_runtime_update_session(db, request).await?;
JsonValue::Null
}
"agent_runtime_delete_session" => {
let args = args_or_default(args);
let session_id = get_string_arg(&args, "sessionId", "session_id")?;
let aster_state = app_handle.state::<crate::agent::AsterAgentState>();
let db = app_handle.state::<crate::database::DbConnection>();
crate::commands::aster_agent_cmd::agent_runtime_delete_session(
app_handle.clone(),
aster_state,
db,
session_id,
)
.await?;
JsonValue::Null
}
"agent_runtime_remove_queued_turn" => {
let request = parse_request::<
crate::commands::aster_agent_cmd::AgentRuntimeRemoveQueuedTurnRequest,
>(args)?;
serde_json::to_value(
crate::commands::aster_agent_cmd::agent_runtime_remove_queued_turn(
app_handle.clone(),
request,
)
.await?,
)?
}
"agent_runtime_promote_queued_turn" => {
let request = parse_request::<
crate::commands::aster_agent_cmd::AgentRuntimePromoteQueuedTurnRequest,
>(args)?;
let aster_state = app_handle.state::<crate::agent::AsterAgentState>();
let db = app_handle.state::<crate::database::DbConnection>();
let api_key_provider_service =
app_handle
.state::<crate::commands::api_key_provider_cmd::ApiKeyProviderServiceState>();
let logs = app_handle.state::<crate::app::LogState>();
let config_manager = app_handle.state::<crate::config::GlobalConfigManagerState>();
let mcp_manager = app_handle.state::<crate::mcp::McpManagerState>();
let automation_state =
app_handle.state::<crate::services::automation_service::AutomationServiceState>();
serde_json::to_value(
crate::commands::aster_agent_cmd::agent_runtime_promote_queued_turn(
app_handle.clone(),
aster_state,
db,
api_key_provider_service,
logs,
config_manager,
mcp_manager,
automation_state,
request,
)
.await?,
)?
}
"agent_runtime_respond_action" => {
let request = parse_request::<
crate::commands::aster_agent_cmd::AgentRuntimeRespondActionRequest,
>(args)?;
let aster_state = app_handle.state::<crate::agent::AsterAgentState>();
crate::commands::aster_agent_cmd::agent_runtime_respond_action(
app_handle.clone(),
aster_state,
request,
)
.await?;
JsonValue::Null
}
_ => unreachable!("已通过前置 matches! 过滤 agent_runtime 命令"),
};
Ok(Some(result))
}
@@ -60,6 +60,13 @@ pub(super) async fn try_handle(
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
serde_json::to_value(service.get_sync_state().await)?
}
"get_all_alias_configs" => {
let guard = state.model_registry.read().await;
let service = guard
.as_ref()
.ok_or_else(|| "模型注册服务未初始化".to_string())?;
serde_json::to_value(service.get_all_alias_configs().await)?
}
"refresh_model_registry" => {
let guard = state.model_registry.read().await;
let service = guard
@@ -0,0 +1,37 @@
use super::require_app_handle;
use crate::dev_bridge::DevBridgeState;
use serde_json::Value as JsonValue;
use tauri::Manager;
type DynError = Box<dyn std::error::Error>;
pub(super) async fn try_handle(
state: &DevBridgeState,
cmd: &str,
_args: Option<&JsonValue>,
) -> Result<Option<JsonValue>, DynError> {
if cmd != "get_plugins_with_ui" {
return Ok(None);
}
let app_handle = require_app_handle(state)?;
let result = match cmd {
"get_plugins_with_ui" => {
let installer_state =
app_handle.state::<crate::commands::plugin_install_cmd::PluginInstallerState>();
let plugin_manager_state =
app_handle.state::<crate::commands::plugin_cmd::PluginManagerState>();
serde_json::to_value(
crate::commands::plugin_cmd::get_plugins_with_ui(
installer_state,
plugin_manager_state,
)
.await?,
)?
}
_ => unreachable!("已通过前置判断过滤插件命令"),
};
Ok(Some(result))
}
@@ -0,0 +1,77 @@
use super::{args_or_default, require_app_handle};
use crate::dev_bridge::DevBridgeState;
use serde_json::Value as JsonValue;
use tauri::Manager;
type DynError = Box<dyn std::error::Error>;
pub(super) async fn try_handle(
state: &DevBridgeState,
cmd: &str,
args: Option<&JsonValue>,
) -> Result<Option<JsonValue>, DynError> {
if cmd != "sync_tray_model_shortcuts" {
return Ok(None);
}
let app_handle = require_app_handle(state)?;
let result = match cmd {
"sync_tray_model_shortcuts" => {
let Some(tray_state) = app_handle.try_state::<crate::TrayManagerState<tauri::Wry>>()
else {
return Ok(Some(JsonValue::Null));
};
let args = args_or_default(args);
let current_model_provider_type = args
.get("currentModelProviderType")
.or_else(|| args.get("current_model_provider_type"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string();
let current_model_provider_label = args
.get("currentModelProviderLabel")
.or_else(|| args.get("current_model_provider_label"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string();
let current_model = args
.get("currentModel")
.or_else(|| args.get("current_model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string();
let current_theme_label = args
.get("currentThemeLabel")
.or_else(|| args.get("current_theme_label"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string();
let quick_model_groups = args
.get("quickModelGroups")
.or_else(|| args.get("quick_model_groups"))
.cloned()
.map(serde_json::from_value::<Vec<crate::tray::TrayQuickModelGroup>>)
.transpose()?
.unwrap_or_default();
match crate::commands::tray_cmd::sync_tray_model_shortcuts(
tray_state,
current_model_provider_type,
current_model_provider_label,
current_model,
current_theme_label,
quick_model_groups,
)
.await
{
Ok(()) => JsonValue::Null,
Err(error) if error.contains("托盘管理器未初始化") => JsonValue::Null,
Err(error) => return Err(error.into()),
}
}
_ => unreachable!("已通过前置判断过滤托盘命令"),
};
Ok(Some(result))
}
+1
View File
@@ -39,6 +39,7 @@ pub use lime_mcp as mcp;
// 核心模块(Tauri 相关业务逻辑)
pub mod agent;
pub mod agent_tools;
pub mod app;
pub mod plugin;
pub mod screenshot;
@@ -212,7 +212,8 @@ mod tests {
content_json TEXT NOT NULL,
timestamp TEXT NOT NULL,
tool_calls_json TEXT,
tool_call_id TEXT
tool_call_id TEXT,
reasoning_content TEXT
);
CREATE TABLE general_chat_sessions (
id TEXT PRIMARY KEY,
@@ -556,7 +556,8 @@ mod tests {
content_json TEXT NOT NULL,
timestamp TEXT NOT NULL,
tool_calls_json TEXT,
tool_call_id TEXT
tool_call_id TEXT,
reasoning_content TEXT
);
CREATE TABLE general_chat_sessions (
id TEXT PRIMARY KEY,