magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
//! Persistable capability ceiling. Applying it only removes current permissions.
use super::{MVP_TOOL_CAPABILITIES, ToolCapability, ToolRuntime};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ToolCeiling {
    cwd: std::path::PathBuf,
    inspection: bool,
    enabled_builtins: BTreeSet<String>,
    disabled_tools: BTreeSet<String>,
    #[serde(default)]
    code_mode_disabled_tools: Option<BTreeSet<String>>,
    read_absolute_paths: bool,
    view_image_absolute_paths: bool,
    hashline_absolute_paths: bool,
    write_absolute_paths: bool,
    grep_absolute_paths: bool,
    find_absolute_paths: bool,
    list_files_absolute_paths: bool,
    ast_grep_absolute_paths: bool,
    bash_absolute_paths: bool,
    bash_shell_expansion: bool,
    subagents_absolute_paths: bool,
    remaining_depth: usize,
}

pub(super) fn inspection_allows(name: &str) -> bool {
    matches!(
        ToolCapability::from_dispatch_name(name),
        Some(
            ToolCapability::Read
                | ToolCapability::Find
                | ToolCapability::ListFiles
                | ToolCapability::Grep
                | ToolCapability::AstGrep
        )
    )
}

impl ToolCeiling {
    pub(crate) fn capture(tools: &ToolRuntime) -> anyhow::Result<Self> {
        Ok(Self {
            cwd: tools.cwd_canonical.clone(),
            inspection: tools.inspection,
            enabled_builtins: MVP_TOOL_CAPABILITIES
                .iter()
                .filter(|tool| !tools.is_tool_disabled(tool.canonical_name()))
                .map(|tool| tool.canonical_name().to_string())
                .collect(),
            disabled_tools: tools.disabled_tool_names()?.into_iter().collect(),
            code_mode_disabled_tools: Some(
                tools
                    .for_code_mode()
                    .disabled_tool_names()?
                    .into_iter()
                    .collect(),
            ),
            read_absolute_paths: tools.read_absolute_paths,
            view_image_absolute_paths: tools.view_image_absolute_paths,
            hashline_absolute_paths: tools.hashline_absolute_paths,
            write_absolute_paths: tools.write_absolute_paths,
            grep_absolute_paths: tools.grep_absolute_paths,
            find_absolute_paths: tools.find_absolute_paths,
            list_files_absolute_paths: tools.list_files_absolute_paths,
            ast_grep_absolute_paths: tools.ast_grep_absolute_paths,
            bash_absolute_paths: tools.bash_absolute_paths,
            bash_shell_expansion: tools.bash_shell_expansion,
            subagents_absolute_paths: tools.subagents_absolute_paths,
            remaining_depth: tools
                .subagents_max_depth
                .saturating_sub(tools.subagent_depth),
        })
    }

    pub(crate) fn validate(&self) -> anyhow::Result<()> {
        anyhow::ensure!(
            self.remaining_depth <= crate::subagents::MAX_SUBAGENT_MAX_DEPTH,
            "invalid task delegation ceiling"
        );
        anyhow::ensure!(
            self.disabled_tools.len() <= 1024
                && self.enabled_builtins.len()
                    <= MVP_TOOL_CAPABILITIES.len()
                        + usize::from(self.enabled_builtins.contains("jev")),
            "oversized task tool ceiling"
        );
        for name in self.enabled_builtins.iter().chain(&self.disabled_tools) {
            anyhow::ensure!(
                name.len() <= 512 && ToolRuntime::canonical_disabled_key(name)? == *name,
                "invalid task tool ceiling"
            );
        }
        if let Some(names) = &self.code_mode_disabled_tools {
            anyhow::ensure!(names.len() <= 1024, "oversized Code Mode tool ceiling");
            for name in names {
                anyhow::ensure!(
                    name.len() <= 512 && ToolRuntime::canonical_disabled_key(name)? == *name,
                    "invalid Code Mode tool ceiling"
                );
            }
        }
        Ok(())
    }

    pub(crate) fn is_inspection(&self) -> bool {
        self.inspection
    }

    pub(crate) fn apply(&self, tools: &mut ToolRuntime) -> anyhow::Result<()> {
        self.validate()?;
        anyhow::ensure!(
            tools.cwd_canonical == self.cwd,
            "delegated task cwd differs from its saved capability ceiling; start a separately authorized task"
        );
        tools.restrict_to_inspection(self.inspection);
        let mut disabled = tools.disabled_tool_names()?;
        disabled.extend(self.disabled_tools.iter().cloned());
        disabled.extend(
            MVP_TOOL_CAPABILITIES
                .iter()
                .filter(|tool| !self.enabled_builtins.contains(tool.canonical_name()))
                .map(|tool| tool.canonical_name().to_string()),
        );
        tools.code_mode.disabled.extend(
            self.code_mode_disabled_tools
                .as_ref()
                .map(|names| names.iter().cloned().collect::<Vec<_>>())
                .unwrap_or_else(|| disabled.iter().cloned().collect()),
        );
        tools.disabled_tools = std::sync::Arc::new(std::sync::Mutex::new(disabled));
        tools.read_absolute_paths &= self.read_absolute_paths;
        tools.view_image_absolute_paths &= self.view_image_absolute_paths;
        tools.hashline_absolute_paths &= self.hashline_absolute_paths;
        tools.write_absolute_paths &= self.write_absolute_paths;
        tools.grep_absolute_paths &= self.grep_absolute_paths;
        tools.find_absolute_paths &= self.find_absolute_paths;
        tools.list_files_absolute_paths &= self.list_files_absolute_paths;
        tools.ast_grep_absolute_paths &= self.ast_grep_absolute_paths;
        tools.bash_absolute_paths &= self.bash_absolute_paths;
        tools.bash_shell_expansion &= self.bash_shell_expansion;
        tools.subagents_absolute_paths &= self.subagents_absolute_paths;
        tools.subagents_max_depth = tools
            .subagents_max_depth
            .min(tools.subagent_depth.saturating_add(self.remaining_depth));
        Ok(())
    }
}