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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion core/src/agent_api/agent_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
);
}
}
7 changes: 6 additions & 1 deletion core/src/permissions/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
108 changes: 91 additions & 17 deletions core/src/tools/task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Arc<dyn PermissionChecker>> {
if has_defined_permissions(agent) {
Some(Arc::new(agent.permissions.clone()) as Arc<dyn PermissionChecker>)
} else {
None
}
}

fn build_child_config(
agent: &AgentDefinition,
params: &TaskParams,
tools: Vec<ToolDefinition>,
) -> 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
Expand Down Expand Up @@ -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<dyn crate::permissions::PermissionChecker>);
}
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, &params, child_executor.definitions());

let tool_context =
ToolContext::new(PathBuf::from(&self.workspace)).with_session_id(session_id.clone());
Expand Down Expand Up @@ -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, &params, 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, &params, 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
);
}
}
Loading