Skip to main content

agentzero_config/
policy.rs

1use 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    // Privacy enforcement: local_only mode disables outbound network tools.
86    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        // Restrict URL access to localhost only.
92        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}