atman-runtime 1.11.0

atman flow execution runtime: evaluator, tool dispatch, provider dispatch, executor, memory stores
Documentation
use std::path::PathBuf;

use crate::error::RuntimeError;
use crate::git::GitCli;
use crate::tool::{ApprovalLevel, BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
use crate::value::Value;

pub struct GitRestore;
pub struct GitRevert;
pub struct GitTagList;
pub struct GitTagCreate;

fn cwd(args: &ToolArgs, ctx: &ToolCtx) -> Result<PathBuf, RuntimeError> {
    let explicit = args.named("cwd").and_then(|value| match value {
        Value::Str(path) => Some(std::path::Path::new(path)),
        _ => None,
    });
    ctx.resolve_cwd(explicit)
}

async fn cwd_mut(args: &ToolArgs, ctx: &ToolCtx, tool: &str) -> Result<PathBuf, RuntimeError> {
    let cwd = cwd(args, ctx)?;
    crate::fs_access::authorize_write(ctx, &cwd, tool, true).await?;
    Ok(cwd)
}

fn string_arg<'a>(args: &'a ToolArgs, name: &str) -> Result<&'a str, RuntimeError> {
    match args.named(name) {
        Some(Value::Str(value)) => Ok(value),
        Some(other) => Err(RuntimeError::TypeMismatch {
            expected: "string".into(),
            actual: other.kind_name().into(),
        }),
        None => Err(RuntimeError::MissingArg(name.into())),
    }
}
fn paths(args: &ToolArgs) -> Result<Vec<String>, RuntimeError> {
    match args.named("paths") {
        Some(Value::List(values)) => values
            .iter()
            .map(|value| match value {
                Value::Str(path) if !path.is_empty() && !path.starts_with('-') => Ok(path.clone()),
                Value::Str(_) => Err(RuntimeError::ToolFailed("git.restore: invalid path".into())),
                other => Err(RuntimeError::TypeMismatch {
                    expected: "list<string>".into(),
                    actual: other.kind_name().into(),
                }),
            })
            .collect(),
        Some(other) => Err(RuntimeError::TypeMismatch {
            expected: "list<string>".into(),
            actual: other.kind_name().into(),
        }),
        None => Err(RuntimeError::MissingArg("paths".into())),
    }
}

impl Tool for GitRestore {
    fn name(&self) -> &str {
        "git.restore"
    }
    fn tier(&self) -> Tier {
        Tier::Two
    }
    fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
        ApprovalLevel::Approve
    }
    fn description(&self) -> Option<&str> {
        Some(
            "Restore explicit paths from HEAD or the index without allowing arbitrary reset/clean operations.",
        )
    }
    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({"type":"object","required":["paths"],"properties":{"paths":{"type":"array","items":{"type":"string"}},"mode":{"type":"string","enum":["worktree","staged","both"],"default":"worktree"},"source":{"type":"string"},"cwd":{"type":"string"}}})
    }
    fn invocation_provenance(
        &self,
        args: &ToolArgs,
        ctx: &ToolCtx,
    ) -> Result<crate::permission::ResourceProvenance, RuntimeError> {
        crate::tools::git_ops::git_mutation_provenance(args, ctx)
    }

    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let cwd = cwd_mut(&args, ctx, self.name()).await?;
            let values = paths(&args)?;
            let mode = match args.named("mode") {
                Some(Value::Str(mode)) => mode.as_str(),
                Some(other) => {
                    return Err(RuntimeError::TypeMismatch {
                        expected: "string".into(),
                        actual: other.kind_name().into(),
                    });
                }
                None => "worktree",
            };
            if !matches!(mode, "worktree" | "staged" | "both") {
                return Err(RuntimeError::ToolFailed(
                    "git.restore: mode must be worktree, staged, or both".into(),
                ));
            }
            let source = match args.named("source") {
                Some(Value::Str(source)) => source.as_str(),
                Some(other) => {
                    return Err(RuntimeError::TypeMismatch {
                        expected: "string".into(),
                        actual: other.kind_name().into(),
                    });
                }
                None => "HEAD",
            };
            let cli = GitCli::at(&cwd);
            let mut command = vec!["restore"];
            if mode == "staged" || mode == "both" {
                command.push("--staged");
            }
            if mode == "worktree" || mode == "both" {
                command.push("--worktree");
            }
            command.push("--source");
            command.push(source);
            command.push("--");
            let refs: Vec<&str> = values.iter().map(String::as_str).collect();
            command.extend(refs);
            cli.run(&command)
                .map_err(|e| RuntimeError::ToolFailed(format!("git.restore: {e}")))?;
            Ok(Value::Struct(vec![
                ("mode".into(), Value::Str(mode.into())),
                (
                    "paths".into(),
                    Value::List(values.into_iter().map(Value::Str).collect()),
                ),
            ]))
        })
    }
}

impl Tool for GitRevert {
    fn name(&self) -> &str {
        "git.revert"
    }
    fn tier(&self) -> Tier {
        Tier::Three
    }
    fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
        ApprovalLevel::Dangerous
    }
    fn description(&self) -> Option<&str> {
        Some("Create a signed/hook-aware revert commit for an explicit revision.")
    }
    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({"type":"object","required":["revision"],"properties":{"revision":{"type":"string"},"cwd":{"type":"string"}}})
    }
    fn invocation_provenance(
        &self,
        args: &ToolArgs,
        ctx: &ToolCtx,
    ) -> Result<crate::permission::ResourceProvenance, RuntimeError> {
        crate::tools::git_ops::git_mutation_provenance(args, ctx)
    }

    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let revision = string_arg(&args, "revision")?;
            if revision.is_empty() || revision.starts_with('-') || revision.contains(' ') {
                return Err(RuntimeError::ToolFailed(
                    "git.revert: invalid revision".into(),
                ));
            }
            GitCli::at(cwd_mut(&args, ctx, self.name()).await?)
                .run(&["revert", "--no-edit", revision])
                .map_err(|e| RuntimeError::ToolFailed(format!("git.revert: {e}")))?;
            Ok(Value::Struct(vec![(
                "revision".into(),
                Value::Str(revision.into()),
            )]))
        })
    }
}

impl Tool for GitTagList {
    fn name(&self) -> &str {
        "git.tag.list"
    }
    fn tier(&self) -> Tier {
        Tier::Zero
    }
    fn description(&self) -> Option<&str> {
        Some("List local Git tags.")
    }
    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({"type":"object","properties":{"cwd":{"type":"string"}}})
    }
    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let output = GitCli::at(cwd(&args, ctx)?)
                .run(&["tag", "--list", "--format=%(refname:short)"])
                .map_err(|e| RuntimeError::ToolFailed(format!("git.tag.list: {e}")))?;
            Ok(Value::List(
                output
                    .lines()
                    .filter(|line| !line.is_empty())
                    .map(|line| Value::Str(line.to_owned()))
                    .collect(),
            ))
        })
    }
}

impl Tool for GitTagCreate {
    fn name(&self) -> &str {
        "git.tag.create"
    }
    fn tier(&self) -> Tier {
        Tier::Three
    }
    fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
        ApprovalLevel::Dangerous
    }
    fn description(&self) -> Option<&str> {
        Some("Create a local tag for an explicit revision.")
    }
    fn input_schema(&self) -> serde_json::Value {
        serde_json::json!({"type":"object","required":["name"],"properties":{"name":{"type":"string"},"revision":{"type":"string","default":"HEAD"},"cwd":{"type":"string"}}})
    }
    fn invocation_provenance(
        &self,
        args: &ToolArgs,
        ctx: &ToolCtx,
    ) -> Result<crate::permission::ResourceProvenance, RuntimeError> {
        crate::tools::git_ops::git_mutation_provenance(args, ctx)
    }

    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
        Box::pin(async move {
            let name = string_arg(&args, "name")?;
            let revision = match args.named("revision") {
                Some(Value::Str(value)) => value.as_str(),
                Some(other) => {
                    return Err(RuntimeError::TypeMismatch {
                        expected: "string".into(),
                        actual: other.kind_name().into(),
                    });
                }
                None => "HEAD",
            };
            if name.is_empty()
                || name.starts_with('-')
                || name.contains("..")
                || name.contains(' ')
                || revision.starts_with('-')
            {
                return Err(RuntimeError::ToolFailed(
                    "git.tag.create: invalid tag or revision".into(),
                ));
            }
            GitCli::at(cwd_mut(&args, ctx, self.name()).await?)
                .run(&["tag", name, revision])
                .map_err(|e| RuntimeError::ToolFailed(format!("git.tag.create: {e}")))?;
            Ok(Value::Struct(vec![
                ("name".into(), Value::Str(name.into())),
                ("revision".into(), Value::Str(revision.into())),
            ]))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::process::Command;

    fn run(dir: &std::path::Path, args: &[&str]) -> String {
        let output = Command::new("git")
            .args(args)
            .current_dir(dir)
            .output()
            .unwrap();
        assert!(
            output.status.success(),
            "git {} failed: {}",
            args.join(" "),
            String::from_utf8_lossy(&output.stderr)
        );
        String::from_utf8_lossy(&output.stdout).trim().to_owned()
    }

    fn repo() -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        run(dir.path(), &["init", "-q", "-b", "main"]);
        run(dir.path(), &["config", "user.name", "test"]);
        run(dir.path(), &["config", "user.email", "test@example.com"]);
        run(dir.path(), &["config", "commit.gpgsign", "false"]);
        std::fs::write(dir.path().join("a.txt"), "one\n").unwrap();
        run(dir.path(), &["add", "."]);
        run(dir.path(), &["commit", "-qm", "initial"]);
        dir
    }

    #[tokio::test]
    async fn restore_tag_and_revert_use_explicit_safe_inputs() {
        let dir = repo();
        std::fs::write(dir.path().join("a.txt"), "changed\n").unwrap();
        let ctx = ToolCtx::default();
        let restore_args = ToolArgs {
            named: vec![
                ("cwd".into(), Value::Str(dir.path().display().to_string())),
                (
                    "paths".into(),
                    Value::List(vec![Value::Str("a.txt".into())]),
                ),
            ],
            ..ToolArgs::default()
        };
        GitRestore.call(restore_args, &ctx).await.unwrap();
        assert_eq!(
            std::fs::read_to_string(dir.path().join("a.txt")).unwrap(),
            "one\n"
        );
        let tag_args = ToolArgs {
            named: vec![
                ("cwd".into(), Value::Str(dir.path().display().to_string())),
                ("name".into(), Value::Str("v1".into())),
            ],
            ..ToolArgs::default()
        };
        GitTagCreate.call(tag_args, &ctx).await.unwrap();
        let tags = GitTagList
            .call(
                ToolArgs {
                    named: vec![("cwd".into(), Value::Str(dir.path().display().to_string()))],
                    ..ToolArgs::default()
                },
                &ctx,
            )
            .await
            .unwrap();
        assert!(
            matches!(tags, Value::List(items) if items.iter().any(|item| matches!(item, Value::Str(value) if value == "v1")))
        );
        std::fs::write(dir.path().join("b.txt"), "two\n").unwrap();
        run(dir.path(), &["add", "b.txt"]);
        run(dir.path(), &["commit", "-qm", "second"]);
        let revision = run(dir.path(), &["rev-parse", "HEAD"]);
        GitRevert
            .call(
                ToolArgs {
                    named: vec![
                        ("cwd".into(), Value::Str(dir.path().display().to_string())),
                        ("revision".into(), Value::Str(revision)),
                    ],
                    ..ToolArgs::default()
                },
                &ctx,
            )
            .await
            .unwrap();
        assert!(!dir.path().join("b.txt").exists());
    }

    #[test]
    fn high_risk_operations_require_dangerous_approval() {
        let ctx = ToolCtx::default();
        assert_eq!(
            GitRevert.approval_level(&ToolArgs::default(), &ctx),
            ApprovalLevel::Dangerous
        );
        assert_eq!(
            GitTagCreate.approval_level(&ToolArgs::default(), &ctx),
            ApprovalLevel::Dangerous
        );
    }
}