agentzero_config/
policy.rs1use crate::loader::load;
2use agentzero_tools::{
3 ReadFilePolicy, ShellPolicy, ToolSecurityPolicy, UrlAccessPolicy, WriteFilePolicy,
4};
5use anyhow::Context;
6use std::path::{Component, Path, PathBuf};
7
8pub fn load_tool_security_policy(
9 workspace_root: &Path,
10 config_path: &Path,
11) -> anyhow::Result<ToolSecurityPolicy> {
12 let config = load(config_path)?;
13 let allowed_root = resolve_allowed_root(workspace_root, &config.security.allowed_root)?;
14
15 let url_cfg = &config.security.url_access;
16 let mut allow_cidrs = Vec::new();
17 for cidr_str in &url_cfg.allow_cidrs {
18 allow_cidrs.push(
19 agentzero_core::common::url_policy::CidrRange::parse(cidr_str).with_context(|| {
20 format!("invalid CIDR in security.url_access.allow_cidrs: {cidr_str}")
21 })?,
22 );
23 }
24
25 let enable_git = config.security.allowed_commands.iter().any(|c| c == "git");
26
27 let mut policy = ToolSecurityPolicy {
28 read_file: ReadFilePolicy {
29 allowed_root: allowed_root.clone(),
30 max_read_bytes: config.security.read_file.max_read_bytes,
31 allow_binary: config.security.read_file.allow_binary,
32 },
33 write_file: WriteFilePolicy {
34 allowed_root,
35 max_write_bytes: config.security.write_file.max_write_bytes,
36 },
37 shell: ShellPolicy {
38 allowed_commands: config.security.allowed_commands,
39 max_args: config.security.shell.max_args,
40 max_arg_length: config.security.shell.max_arg_length,
41 max_output_bytes: config.security.shell.max_output_bytes,
42 forbidden_chars: config.security.shell.forbidden_chars.clone(),
43 command_policy: if config.security.shell.context_aware_parsing {
44 Some(
45 agentzero_tools::shell::ShellCommandPolicy::from_legacy_forbidden_chars(
46 &config.security.shell.forbidden_chars,
47 ),
48 )
49 } else {
50 None
51 },
52 },
53 url_access: UrlAccessPolicy {
54 block_private_ip: url_cfg.block_private_ip,
55 allow_loopback: url_cfg.allow_loopback,
56 allow_cidrs,
57 allow_domains: url_cfg.allow_domains.clone(),
58 enforce_domain_allowlist: url_cfg.enforce_domain_allowlist,
59 domain_allowlist: url_cfg.domain_allowlist.clone(),
60 domain_blocklist: url_cfg.domain_blocklist.clone(),
61 approved_domains: url_cfg.approved_domains.clone(),
62 require_first_visit_approval: url_cfg.require_first_visit_approval,
63 },
64 enable_write_file: config.security.write_file.enabled,
65 enable_mcp: config.security.mcp.enabled,
66 allowed_mcp_servers: config.security.mcp.allowed_servers,
67 enable_process_plugin: config.security.plugin.enabled,
68 enable_git,
69 enable_cron: true,
70 enable_web_search: config.web_search.enabled,
71 enable_browser: config.browser.enabled,
72 enable_browser_open: config.browser.enabled,
73 enable_http_request: config.http_request.enabled,
74 enable_web_fetch: config.web_fetch.enabled,
75 enable_url_validation: true,
76 enable_agents_ipc: true,
77 enable_composio: config.composio.enabled,
78 enable_pushover: config.pushover.enabled,
79 enable_wasm_plugins: config.security.plugin.wasm_enabled,
80 wasm_global_plugin_dir: config.security.plugin.global_plugin_dir.map(PathBuf::from),
81 wasm_project_plugin_dir: config.security.plugin.project_plugin_dir.map(PathBuf::from),
82 wasm_dev_plugin_dir: config.security.plugin.dev_plugin_dir.map(PathBuf::from),
83 };
84
85 if config.privacy.mode == "local_only" {
87 policy.enable_http_request = false;
88 policy.enable_web_fetch = false;
89 policy.enable_web_search = false;
90 policy.enable_composio = false;
91 policy.url_access.allow_loopback = true;
93 policy.url_access.block_private_ip = false;
94 policy.url_access.enforce_domain_allowlist = true;
95 policy.url_access.domain_allowlist = vec![
96 "localhost".to_string(),
97 "127.0.0.1".to_string(),
98 "::1".to_string(),
99 ];
100 }
101
102 Ok(policy)
103}
104
105#[derive(Debug, Clone)]
106pub struct AuditPolicy {
107 pub enabled: bool,
108 pub path: PathBuf,
109}
110
111pub fn load_audit_policy(workspace_root: &Path, config_path: &Path) -> anyhow::Result<AuditPolicy> {
112 let config = load(config_path)?;
113
114 if !config.security.audit.enabled {
115 return Ok(AuditPolicy {
116 enabled: false,
117 path: resolve_path(workspace_root, "./agentzero-audit.log"),
118 });
119 }
120
121 Ok(AuditPolicy {
122 enabled: true,
123 path: resolve_path(workspace_root, &config.security.audit.path),
124 })
125}
126
127fn resolve_allowed_root(workspace_root: &Path, configured_root: &str) -> anyhow::Result<PathBuf> {
128 let configured_path = Path::new(configured_root);
129 if configured_path.is_relative()
130 && configured_path
131 .components()
132 .any(|component| matches!(component, Component::ParentDir))
133 {
134 anyhow::bail!("security.allowed_root must not contain parent directory traversal");
135 }
136
137 let path = Path::new(configured_root);
138 let candidate = if path.is_absolute() {
139 path.to_path_buf()
140 } else {
141 workspace_root.join(path)
142 };
143
144 candidate.canonicalize().with_context(|| {
145 format!(
146 "security.allowed_root does not exist: {}",
147 candidate.display()
148 )
149 })
150}
151
152fn resolve_path(workspace_root: &Path, configured: &str) -> PathBuf {
153 let path = Path::new(configured);
154 if path.is_absolute() {
155 path.to_path_buf()
156 } else {
157 workspace_root.join(path)
158 }
159}