mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
release: v0.92.0
This commit is contained in:
@@ -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 |
|
||||
|
||||
## 凭证池桥接
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
pub mod catalog;
|
||||
pub mod execution;
|
||||
pub mod inventory;
|
||||
+19
-11
@@ -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
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user