feat(capability): 扩展 CapabilityPolicy 支持子代理黑白名单
- domain: CapabilityPolicy 新增 allowed_subagents/denied_subagents 字段及 check_subagent_allowed 方法 - experts: 专家 frontmatter 解析/渲染支持子代理策略字段 - task/runtime: spawn/resume 双路径校验父代理子代理策略;新增 update_subagent 写回 SUBAGENT.md(与 update_expert 对称);新增 SubagentPromptProvider 按专家策略过滤子代理索引 - task/runtime: 子代理自身 capability 作为孙代理的 parent_capability 透传(ToolContext),保持解耦 - traits: ToolContext 新增 parent_capability 字段 - agent_factory: 主 agent 注入 expert_capability 到 ToolContext 安全:策略不通过即拒绝(与 def 不可用即拒绝范式一致),防止 LLM 通过选择被禁子代理绕过限制;max_nesting_depth 兜底防递归不可被 def 覆盖。
This commit is contained in:
parent
9381ed5dd4
commit
c27efedb6c
@ -3,12 +3,14 @@ pub mod tools;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// 角色能力策略:工具与技能的白/黑名单。全为空表示沿用默认(不过滤)。
|
/// 角色能力策略:工具、技能、子代理的白/黑名单。全为空表示沿用默认(不过滤)。
|
||||||
///
|
///
|
||||||
/// 生效顺序:先白名单取交集,再黑名单扣除。专家与子代理共用此结构,
|
/// 生效顺序:先白名单取交集,再黑名单扣除。专家与子代理共用此结构,
|
||||||
/// 确保语义一致。MCP 工具注册在 `ToolRegistry` 中(名 `mcp_*`),与内置
|
/// 确保语义一致。MCP 工具注册在 `ToolRegistry` 中(名 `mcp_*`),与内置
|
||||||
/// 工具同源,因此 `allowed_tools`/`denied_tools` 覆盖内置 + MCP 工具;
|
/// 工具同源,因此 `allowed_tools`/`denied_tools` 覆盖内置 + MCP 工具;
|
||||||
/// `allowed_skills`/`denied_skills` 仅覆盖 SKILL.md 技能。
|
/// `allowed_skills`/`denied_skills` 仅覆盖 SKILL.md 技能;
|
||||||
|
/// `allowed_subagents`/`denied_subagents` 覆盖子代理加载(通过 ToolContext
|
||||||
|
/// 传递给 TaskTool,在 spawn/resume 时强制校验)。
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct CapabilityPolicy {
|
pub struct CapabilityPolicy {
|
||||||
/// 技能白名单:`None` = 不限;`Some(vec)` = 仅这些 SKILL.md 技能可见。
|
/// 技能白名单:`None` = 不限;`Some(vec)` = 仅这些 SKILL.md 技能可见。
|
||||||
@ -24,6 +26,13 @@ pub struct CapabilityPolicy {
|
|||||||
/// 工具黑名单(含 `mcp_*` 工具)。
|
/// 工具黑名单(含 `mcp_*` 工具)。
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub denied_tools: Vec<String>,
|
pub denied_tools: Vec<String>,
|
||||||
|
/// 子代理白名单:`None` = 不限;`Some(vec)` = 仅这些子代理可被加载。
|
||||||
|
/// 通过 ToolContext 传递给 TaskTool,在 spawn/resume 时强制校验。
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub allowed_subagents: Option<Vec<String>>,
|
||||||
|
/// 子代理黑名单:禁止加载这些子代理。空表示不禁。
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub denied_subagents: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CapabilityPolicy {
|
impl CapabilityPolicy {
|
||||||
@ -33,6 +42,8 @@ impl CapabilityPolicy {
|
|||||||
&& self.denied_skills.is_empty()
|
&& self.denied_skills.is_empty()
|
||||||
&& self.allowed_tools.is_none()
|
&& self.allowed_tools.is_none()
|
||||||
&& self.denied_tools.is_empty()
|
&& self.denied_tools.is_empty()
|
||||||
|
&& self.allowed_subagents.is_none()
|
||||||
|
&& self.denied_subagents.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 是否声明了任何技能策略。
|
/// 是否声明了任何技能策略。
|
||||||
@ -44,4 +55,28 @@ impl CapabilityPolicy {
|
|||||||
pub fn has_tool_policy(&self) -> bool {
|
pub fn has_tool_policy(&self) -> bool {
|
||||||
self.allowed_tools.is_some() || !self.denied_tools.is_empty()
|
self.allowed_tools.is_some() || !self.denied_tools.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 是否声明了任何子代理策略。
|
||||||
|
pub fn has_subagent_policy(&self) -> bool {
|
||||||
|
self.allowed_subagents.is_some() || !self.denied_subagents.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验指定子代理是否被允许。返回 Err 时附带拒绝原因。
|
||||||
|
pub fn check_subagent_allowed(&self, name: &str) -> Result<(), String> {
|
||||||
|
if let Some(list) = &self.allowed_subagents {
|
||||||
|
if !list.iter().any(|s| s == name) {
|
||||||
|
return Err(format!(
|
||||||
|
"subagent '{}' is not in the allowed_subagents whitelist",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.denied_subagents.iter().any(|s| s == name) {
|
||||||
|
return Err(format!(
|
||||||
|
"subagent '{}' is in the denied_subagents blacklist",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -738,6 +738,10 @@ struct ExpertFrontmatter {
|
|||||||
allowed_tools: Option<Vec<String>>,
|
allowed_tools: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
denied_tools: Vec<String>,
|
denied_tools: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
allowed_subagents: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
denied_subagents: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 规范化字符串列表:去除空白项与首尾空格。
|
/// 规范化字符串列表:去除空白项与首尾空格。
|
||||||
@ -775,6 +779,10 @@ fn render_expert_file(
|
|||||||
allowed_tools: Option<Vec<String>>,
|
allowed_tools: Option<Vec<String>>,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
denied_tools: Vec<String>,
|
denied_tools: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
allowed_subagents: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
denied_subagents: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let yaml = serde_yaml::to_string(&ExpertFrontmatterOwned {
|
let yaml = serde_yaml::to_string(&ExpertFrontmatterOwned {
|
||||||
@ -784,6 +792,8 @@ fn render_expert_file(
|
|||||||
denied_skills: capability.denied_skills.clone(),
|
denied_skills: capability.denied_skills.clone(),
|
||||||
allowed_tools: capability.allowed_tools.clone(),
|
allowed_tools: capability.allowed_tools.clone(),
|
||||||
denied_tools: capability.denied_tools.clone(),
|
denied_tools: capability.denied_tools.clone(),
|
||||||
|
allowed_subagents: capability.allowed_subagents.clone(),
|
||||||
|
denied_subagents: capability.denied_subagents.clone(),
|
||||||
})
|
})
|
||||||
.map_err(|err| format!("failed to render expert frontmatter: {}", err))?;
|
.map_err(|err| format!("failed to render expert frontmatter: {}", err))?;
|
||||||
|
|
||||||
@ -876,6 +886,8 @@ fn parse_expert_file(path: &Path, source: ExpertSource) -> Result<Expert, String
|
|||||||
denied_skills: normalize_string_list(frontmatter.denied_skills),
|
denied_skills: normalize_string_list(frontmatter.denied_skills),
|
||||||
allowed_tools: normalize_optional_list(frontmatter.allowed_tools),
|
allowed_tools: normalize_optional_list(frontmatter.allowed_tools),
|
||||||
denied_tools: normalize_string_list(frontmatter.denied_tools),
|
denied_tools: normalize_string_list(frontmatter.denied_tools),
|
||||||
|
allowed_subagents: normalize_optional_list(frontmatter.allowed_subagents),
|
||||||
|
denied_subagents: normalize_string_list(frontmatter.denied_subagents),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Expert {
|
Ok(Expert {
|
||||||
|
|||||||
@ -34,8 +34,8 @@ pub(crate) fn build_system_prompt_provider(
|
|||||||
prompt_repository,
|
prompt_repository,
|
||||||
)),
|
)),
|
||||||
Box::new(SkillPromptProvider::new(skills, experts.clone())),
|
Box::new(SkillPromptProvider::new(skills, experts.clone())),
|
||||||
Box::new(ExpertPromptProvider::new(experts)),
|
Box::new(ExpertPromptProvider::new(experts.clone())),
|
||||||
Box::new(SubagentPromptProvider::new(subagent_runtime)),
|
Box::new(SubagentPromptProvider::new(subagent_runtime, experts)),
|
||||||
Box::new(ToolPromptProvider::new()),
|
Box::new(ToolPromptProvider::new()),
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
@ -116,17 +116,22 @@ impl AgentFactory {
|
|||||||
self.subagent_runtime.clone(),
|
self.subagent_runtime.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 读取所选专家的 capability(用于工具过滤 + 子代理策略注入 ToolContext)
|
||||||
|
let expert_capability = self
|
||||||
|
.experts
|
||||||
|
.selected_expert_for(&session_id)
|
||||||
|
.map(|e| e.capability);
|
||||||
|
|
||||||
// 按所选专家的工具策略过滤工具集(含内置 + MCP 工具)。
|
// 按所选专家的工具策略过滤工具集(含内置 + MCP 工具)。
|
||||||
// 无专家或专家未声明工具策略时,复用共享的 Arc<ToolRegistry>(零拷贝)。
|
// 无专家或专家未声明工具策略时,复用共享的 Arc<ToolRegistry>(零拷贝)。
|
||||||
let base_tool_count = self.tools.tool_names().len();
|
let base_tool_count = self.tools.tool_names().len();
|
||||||
let tools: Arc<ToolRegistry> = match self.experts.selected_expert_for(&session_id) {
|
let tools: Arc<ToolRegistry> = match &expert_capability {
|
||||||
Some(expert) if expert.capability.has_tool_policy() => {
|
Some(cap) if cap.has_tool_policy() => {
|
||||||
let filtered = self.build_filtered_registry(&expert.capability);
|
let filtered = self.build_filtered_registry(cap);
|
||||||
let filtered_count = filtered.tool_names().len();
|
let filtered_count = filtered.tool_names().len();
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
instance_id = self.instance_id,
|
instance_id = self.instance_id,
|
||||||
session_id = %session_id,
|
session_id = %session_id,
|
||||||
expert = %expert.name,
|
|
||||||
base_tool_count,
|
base_tool_count,
|
||||||
filtered_tool_count = filtered_count,
|
filtered_tool_count = filtered_count,
|
||||||
"AgentFactory: applied expert tool policy"
|
"AgentFactory: applied expert tool policy"
|
||||||
@ -160,6 +165,8 @@ impl AgentFactory {
|
|||||||
task_id: None,
|
task_id: None,
|
||||||
parent_task_id: None,
|
parent_task_id: None,
|
||||||
tool_call_id: None,
|
tool_call_id: None,
|
||||||
|
// 注入专家 capability,TaskTool 据此强制校验子代理白/黑名单
|
||||||
|
parent_capability: expert_capability.clone(),
|
||||||
});
|
});
|
||||||
// 如果有取消信号接收端,注入 Agent
|
// 如果有取消信号接收端,注入 Agent
|
||||||
if let Some(token) = request.cancel_token {
|
if let Some(token) = request.cancel_token {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ use crate::bus::MessageBus;
|
|||||||
use crate::domain::CapabilityPolicy;
|
use crate::domain::CapabilityPolicy;
|
||||||
use crate::providers::StreamDelta;
|
use crate::providers::StreamDelta;
|
||||||
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
||||||
|
use crate::experts::ExpertRuntime;
|
||||||
use crate::skills::SkillRuntime;
|
use crate::skills::SkillRuntime;
|
||||||
use crate::storage::{ConversationRepository, SessionStore};
|
use crate::storage::{ConversationRepository, SessionStore};
|
||||||
use crate::tools::{ToolContext, ToolRegistry};
|
use crate::tools::{ToolContext, ToolRegistry};
|
||||||
@ -487,6 +488,9 @@ impl DefaultSubAgentRuntime {
|
|||||||
task_id: Some(session.id.clone()),
|
task_id: Some(session.id.clone()),
|
||||||
parent_task_id,
|
parent_task_id,
|
||||||
tool_call_id: None,
|
tool_call_id: None,
|
||||||
|
// 子代理自身的 capability 作为孙代理的 parent_capability,
|
||||||
|
// 使孙代理的 TaskTool 能按此策略校验(与主 agent 注入专家 capability 同构)
|
||||||
|
parent_capability: def.map(|d| d.capability.clone()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果有 MessageBus,附加实时广播 emitter
|
// 如果有 MessageBus,附加实时广播 emitter
|
||||||
@ -627,12 +631,21 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| TaskError::MissingContext("channel_name".to_string()))?;
|
.ok_or_else(|| TaskError::MissingContext("channel_name".to_string()))?;
|
||||||
|
|
||||||
// 2. 查找子代理定义
|
// 2. 校验父智能体的子代理策略(白/黑名单),再查找子代理定义。
|
||||||
|
// 与 find_subagent_def 的"def 不可用即拒绝"安全范式一致:策略不通过即拒绝,
|
||||||
|
// 防止 LLM 通过选择被禁子代理绕过限制。
|
||||||
|
if let Some(cap) = &parent_context.parent_capability {
|
||||||
|
if let Err(msg) = cap.check_subagent_allowed(&task.subagent_type.name) {
|
||||||
|
return Err(TaskError::InvalidArguments(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 查找子代理定义
|
||||||
let def = self
|
let def = self
|
||||||
.find_subagent_def(task.subagent_type.as_str())
|
.find_subagent_def(task.subagent_type.as_str())
|
||||||
.map_err(TaskError::InvalidArguments)?;
|
.map_err(TaskError::InvalidArguments)?;
|
||||||
|
|
||||||
// 3. 创建任务会话
|
// 4. 创建任务会话
|
||||||
let topic_id = parent_context.topic_id.clone();
|
let topic_id = parent_context.topic_id.clone();
|
||||||
let session = TaskSession::new(
|
let session = TaskSession::new(
|
||||||
session_id,
|
session_id,
|
||||||
@ -807,7 +820,16 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
&additional_prompt,
|
&additional_prompt,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4.1 重新解析 def 以应用工具过滤。
|
// 4.1 校验父智能体的子代理策略(白/黑名单)。
|
||||||
|
// 安全要求:与 spawn 一致,防止 resume 绕过白名单。若用户切换到不允许
|
||||||
|
// 该子代理的专家,resume 应失败(与 def 被删除即失败的安全语义一致)。
|
||||||
|
if let Some(cap) = &parent_context.parent_capability {
|
||||||
|
if let Err(msg) = cap.check_subagent_allowed(&session.subagent_type) {
|
||||||
|
return Err(TaskError::InvalidArguments(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.2 重新解析 def 以应用工具过滤。
|
||||||
// 安全要求:def 被删除/禁用时必须失败恢复,而不是降级为完整工具集——
|
// 安全要求:def 被删除/禁用时必须失败恢复,而不是降级为完整工具集——
|
||||||
// 否则一个受限子代理(如 allowed_tools: [read])在 def 失踪后会获得全部工具,
|
// 否则一个受限子代理(如 allowed_tools: [read])在 def 失踪后会获得全部工具,
|
||||||
// 构成权限提升。与 spawn 保持一致:def 不可用即拒绝执行。
|
// 构成权限提升。与 spawn 保持一致:def 不可用即拒绝执行。
|
||||||
@ -1243,6 +1265,53 @@ impl SubagentRuntime {
|
|||||||
Some(prompt)
|
Some(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 生成按 capability 过滤后的系统索引提示词。
|
||||||
|
/// 在禁用项过滤之上,再按 `allowed_subagents`(白名单取交集)和
|
||||||
|
/// `denied_subagents`(黑名单扣除)过滤。用于专家/子代理的子代理策略。
|
||||||
|
pub fn system_index_prompt_filtered_with_policy(
|
||||||
|
&self,
|
||||||
|
allowed: Option<&[String]>,
|
||||||
|
denied: &[String],
|
||||||
|
) -> Option<String> {
|
||||||
|
let state = self.disable_state.read().expect("subagent state rwlock poisoned");
|
||||||
|
let catalog = self.catalog.read().expect("subagent catalog rwlock poisoned");
|
||||||
|
let available_defs: Vec<&SubagentDef> = catalog
|
||||||
|
.all()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|def| !state.is_disabled(&def.name))
|
||||||
|
.filter(|def| {
|
||||||
|
if let Some(list) = allowed {
|
||||||
|
list.iter().any(|s| s == &def.name)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(|def| !denied.iter().any(|s| s == &def.name))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if available_defs.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prompt = String::from(
|
||||||
|
"# 子代理系统\n\n\
|
||||||
|
子代理是专用的执行单元,用于处理特定类型的任务。\n\
|
||||||
|
创建子代理任务时,可以选择以下类型之一:\n\n\
|
||||||
|
<available_subagents>\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
for def in available_defs {
|
||||||
|
prompt.push_str(&format!(
|
||||||
|
" <subagent>\n <name>{}</name>\n <description>{}</description>\n </subagent>\n",
|
||||||
|
xml_escape(&def.name),
|
||||||
|
xml_escape(&def.description),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt.push_str("</available_subagents>");
|
||||||
|
Some(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
/// 禁用子代理
|
/// 禁用子代理
|
||||||
pub fn disable_subagent(
|
pub fn disable_subagent(
|
||||||
&self,
|
&self,
|
||||||
@ -1383,24 +1452,42 @@ impl SubagentRuntime {
|
|||||||
/// 为子代理系统提供索引提示词
|
/// 为子代理系统提供索引提示词
|
||||||
///
|
///
|
||||||
/// 负责提供过滤禁用项后的子代理系统索引提示词,注入主 agent。
|
/// 负责提供过滤禁用项后的子代理系统索引提示词,注入主 agent。
|
||||||
|
/// 当会话选中了带子代理策略的专家时,按专家 `CapabilityPolicy` 过滤子代理索引
|
||||||
|
/// (与 `SkillPromptProvider` 过滤技能索引的模式同构)。
|
||||||
pub struct SubagentPromptProvider {
|
pub struct SubagentPromptProvider {
|
||||||
runtime: Arc<SubagentRuntime>,
|
runtime: Arc<SubagentRuntime>,
|
||||||
|
experts: Arc<ExpertRuntime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubagentPromptProvider {
|
impl SubagentPromptProvider {
|
||||||
pub fn new(runtime: Arc<SubagentRuntime>) -> Self {
|
pub fn new(runtime: Arc<SubagentRuntime>, experts: Arc<ExpertRuntime>) -> Self {
|
||||||
Self { runtime }
|
Self { runtime, experts }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemPromptProvider for SubagentPromptProvider {
|
impl SystemPromptProvider for SubagentPromptProvider {
|
||||||
fn build(&self, _context: &SystemPromptContext) -> Option<SystemPrompt> {
|
fn build(&self, context: &SystemPromptContext) -> Option<SystemPrompt> {
|
||||||
self.runtime
|
// 读取所选专家的子代理策略;无专家或无策略时走全局索引(主智能体默认)
|
||||||
.system_index_prompt_filtered()
|
let content = match context.session_id.as_deref() {
|
||||||
.map(|content| SystemPrompt {
|
Some(sid) => {
|
||||||
content,
|
let policy = self
|
||||||
context: Some("subagents".to_string()),
|
.experts
|
||||||
})
|
.selected_expert_for(sid)
|
||||||
|
.map(|e| e.capability);
|
||||||
|
match policy {
|
||||||
|
Some(p) if p.has_subagent_policy() => self.runtime.system_index_prompt_filtered_with_policy(
|
||||||
|
p.allowed_subagents.as_deref(),
|
||||||
|
&p.denied_subagents,
|
||||||
|
),
|
||||||
|
_ => self.runtime.system_index_prompt_filtered(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => self.runtime.system_index_prompt_filtered(),
|
||||||
|
};
|
||||||
|
content.map(|c| SystemPrompt {
|
||||||
|
content: c,
|
||||||
|
context: Some("subagents".to_string()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1473,6 +1560,10 @@ struct SubagentFrontmatter {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
denied_tools: Vec<String>,
|
denied_tools: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
allowed_subagents: Option<Vec<String>>,
|
||||||
|
#[serde(default)]
|
||||||
|
denied_subagents: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
max_execution_secs: Option<u64>,
|
max_execution_secs: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1562,6 +1653,8 @@ fn parse_subagent_file(path: &Path, source: SubagentSource) -> Result<SubagentDe
|
|||||||
denied_skills: frontmatter.denied_skills,
|
denied_skills: frontmatter.denied_skills,
|
||||||
allowed_tools: frontmatter.allowed_tools,
|
allowed_tools: frontmatter.allowed_tools,
|
||||||
denied_tools: frontmatter.denied_tools,
|
denied_tools: frontmatter.denied_tools,
|
||||||
|
allowed_subagents: frontmatter.allowed_subagents,
|
||||||
|
denied_subagents: frontmatter.denied_subagents,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(SubagentDef {
|
Ok(SubagentDef {
|
||||||
@ -1605,6 +1698,10 @@ fn render_subagent_file(
|
|||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
denied_tools: Vec<String>,
|
denied_tools: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
allowed_subagents: Option<Vec<String>>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
denied_subagents: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
max_execution_secs: Option<u64>,
|
max_execution_secs: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1620,6 +1717,8 @@ fn render_subagent_file(
|
|||||||
denied_skills: capability.denied_skills.clone(),
|
denied_skills: capability.denied_skills.clone(),
|
||||||
allowed_tools: capability.allowed_tools.clone(),
|
allowed_tools: capability.allowed_tools.clone(),
|
||||||
denied_tools: capability.denied_tools.clone(),
|
denied_tools: capability.denied_tools.clone(),
|
||||||
|
allowed_subagents: capability.allowed_subagents.clone(),
|
||||||
|
denied_subagents: capability.denied_subagents.clone(),
|
||||||
max_execution_secs,
|
max_execution_secs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::domain::CapabilityPolicy;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ToolResult {
|
pub struct ToolResult {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
@ -26,6 +28,10 @@ pub struct ToolContext {
|
|||||||
pub parent_task_id: Option<String>,
|
pub parent_task_id: Option<String>,
|
||||||
/// 当前工具调用的 ID(由 agent_loop 在执行前注入,用于精确关联 TaskStarted 事件)
|
/// 当前工具调用的 ID(由 agent_loop 在执行前注入,用于精确关联 TaskStarted 事件)
|
||||||
pub tool_call_id: Option<String>,
|
pub tool_call_id: Option<String>,
|
||||||
|
/// 父智能体(主 agent 所选专家或上级子代理)的 capability 策略快照。
|
||||||
|
/// TaskTool 据此强制校验子代理加载(白/黑名单),与 spawn/resume 安全范式一致。
|
||||||
|
/// 以数据形式传递,避免 task 模块反向依赖 experts 模块。
|
||||||
|
pub parent_capability: Option<CapabilityPolicy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user