From c13a148b8bdd58fbc2c7545d2e3ec0018b0c4524 Mon Sep 17 00:00:00 2001 From: lxt <1193027052@qq.com> Date: Wed, 13 May 2026 23:11:56 +0800 Subject: [PATCH] Fix delegated task permission propagation --- core/src/agent_api/agent_binding.rs | 23 +++++- core/src/permissions/policy.rs | 7 +- core/src/tools/task.rs | 108 +++++++++++++++++++++++----- 3 files changed, 119 insertions(+), 19 deletions(-) diff --git a/core/src/agent_api/agent_binding.rs b/core/src/agent_api/agent_binding.rs index 7165dfb..43f3305 100644 --- a/core/src/agent_api/agent_binding.rs +++ b/core/src/agent_api/agent_binding.rs @@ -27,7 +27,7 @@ fn apply_permissions(opts: &mut SessionOptions, def: &AgentDefinition) { } fn has_defined_permissions(def: &AgentDefinition) -> bool { - !def.permissions.allow.is_empty() || !def.permissions.deny.is_empty() + !def.permissions.is_default_policy() } fn apply_step_budget(opts: &mut SessionOptions, def: &AgentDefinition) { @@ -129,4 +129,25 @@ mod tests { assert_eq!(opts.model.as_deref(), Some("anthropic/claude-opus")); } + + #[test] + fn applies_default_allow_agent_permissions() { + use crate::permissions::PermissionDecision; + + let permissions = PermissionPolicy { + default_decision: PermissionDecision::Allow, + ..PermissionPolicy::new() + }; + let def = AgentDefinition::new("worker", "Allowed worker").with_permissions(permissions); + + let opts = apply_agent_definition(SessionOptions::new(), &def); + + let checker = opts + .permission_checker + .expect("non-default permission policy should be applied"); + assert_eq!( + checker.check("bash", &serde_json::json!({"command": "echo ok"})), + PermissionDecision::Allow + ); + } } diff --git a/core/src/permissions/policy.rs b/core/src/permissions/policy.rs index eec70eb..cc6159a 100644 --- a/core/src/permissions/policy.rs +++ b/core/src/permissions/policy.rs @@ -9,7 +9,7 @@ use super::{MatchingRules, PermissionChecker, PermissionDecision, PermissionRule /// 2. Allow rules - any match results in auto-approval /// 3. Ask rules - any match requires user confirmation /// 4. Default - falls back to default_decision -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PermissionPolicy { /// Rules that always deny (checked first) #[serde(default)] @@ -58,6 +58,11 @@ impl PermissionPolicy { Self::default() } + /// Return true when this policy is still the implicit default Ask policy. + pub fn is_default_policy(&self) -> bool { + self == &Self::default() + } + /// Create a strict policy that asks for everything pub fn strict() -> Self { Self { diff --git a/core/src/tools/task.rs b/core/src/tools/task.rs index 16acea3..f4ce986 100644 --- a/core/src/tools/task.rs +++ b/core/src/tools/task.rs @@ -15,9 +15,10 @@ //! ``` use crate::agent::{AgentConfig, AgentEvent, AgentLoop}; -use crate::llm::LlmClient; +use crate::llm::{LlmClient, ToolDefinition}; use crate::mcp::manager::McpManager; -use crate::subagent::AgentRegistry; +use crate::permissions::PermissionChecker; +use crate::subagent::{AgentDefinition, AgentRegistry}; use crate::tools::types::{Tool, ToolContext, ToolOutput}; use anyhow::{Context, Result}; use async_trait::async_trait; @@ -128,6 +129,39 @@ fn format_task_result_for_context(result: &TaskResult) -> (String, bool) { (formatted, truncated) } +fn has_defined_permissions(agent: &AgentDefinition) -> bool { + !agent.permissions.is_default_policy() +} + +fn agent_permission_checker(agent: &AgentDefinition) -> Option> { + if has_defined_permissions(agent) { + Some(Arc::new(agent.permissions.clone()) as Arc) + } else { + None + } +} + +fn build_child_config( + agent: &AgentDefinition, + params: &TaskParams, + tools: Vec, +) -> AgentConfig { + let mut prompt_slots = crate::prompts::SystemPromptSlots::default(); + if let Some(ref p) = agent.prompt { + prompt_slots.extra = Some(p.clone()); + } + + AgentConfig { + prompt_slots, + tools, + max_tool_rounds: params + .max_steps + .unwrap_or_else(|| agent.max_steps.unwrap_or(20)), + permission_checker: agent_permission_checker(agent), + ..AgentConfig::default() + } +} + /// Task executor for delegated child runs. pub struct TaskExecutor { /// Agent registry for looking up agent definitions @@ -217,26 +251,13 @@ impl TaskExecutor { } } - if !agent.permissions.allow.is_empty() || !agent.permissions.deny.is_empty() { + if has_defined_permissions(&agent) { child_executor.set_guard_policy(Arc::new(agent.permissions.clone()) as Arc); } let child_executor = Arc::new(child_executor); - // Inject the agent system prompt via the extra slot. - let mut prompt_slots = crate::prompts::SystemPromptSlots::default(); - if let Some(ref p) = agent.prompt { - prompt_slots.extra = Some(p.clone()); - } - - let child_config = AgentConfig { - prompt_slots, - tools: child_executor.definitions(), - max_tool_rounds: params - .max_steps - .unwrap_or_else(|| agent.max_steps.unwrap_or(20)), - ..AgentConfig::default() - }; + let child_config = build_child_config(&agent, ¶ms, child_executor.definitions()); let tool_context = ToolContext::new(PathBuf::from(&self.workspace)).with_session_id(session_id.clone()); @@ -1256,4 +1277,57 @@ mod tests { assert!(props.get("permissive").is_none()); } + + #[test] + fn test_child_config_inherits_agent_permissions() { + use crate::permissions::{PermissionDecision, PermissionPolicy}; + + let agent = AgentDefinition::new("worker", "Worker") + .with_permissions(PermissionPolicy::new().allow("bash(echo:*)")); + let params = TaskParams { + agent: "worker".to_string(), + description: "Run allowed command".to_string(), + prompt: "Use bash".to_string(), + background: false, + max_steps: None, + }; + + let config = build_child_config(&agent, ¶ms, Vec::new()); + + let checker = config + .permission_checker + .expect("agent permission policy should be installed"); + assert_eq!( + checker.check("bash", &serde_json::json!({"command": "echo ok"})), + PermissionDecision::Allow + ); + } + + #[test] + fn test_child_config_inherits_default_allow_permissions() { + use crate::permissions::{PermissionDecision, PermissionPolicy}; + + let permissions = PermissionPolicy { + default_decision: PermissionDecision::Allow, + ..PermissionPolicy::new() + }; + let agent = AgentDefinition::new("worker", "Worker").with_permissions(permissions); + let params = TaskParams { + agent: "worker".to_string(), + description: "Run any command".to_string(), + prompt: "Use bash".to_string(), + background: false, + max_steps: None, + }; + + let config = build_child_config(&agent, ¶ms, Vec::new()); + + let checker = config + .permission_checker + .expect("non-default permission policy should be installed"); + assert_eq!( + checker.check("bash", &serde_json::json!({"command": "echo ok"})), + PermissionDecision::Allow + ); + } }