diff options
Diffstat (limited to 'crates/atuin-ai/src/permissions/writer.rs')
| -rw-r--r-- | crates/atuin-ai/src/permissions/writer.rs | 199 |
1 files changed, 0 insertions, 199 deletions
diff --git a/crates/atuin-ai/src/permissions/writer.rs b/crates/atuin-ai/src/permissions/writer.rs deleted file mode 100644 index ffef404e..00000000 --- a/crates/atuin-ai/src/permissions/writer.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::path::Path; - -use eyre::Result; - -use crate::permissions::rule::Rule; - -/// Whether a rule should be added to the allow or deny list. -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub(crate) enum RuleDisposition { - Allow, - Deny, -} - -/// Write a permission rule to a `permissions.ai.toml` file. -/// -/// If the file doesn't exist it is created (along with parent directories). -/// If it does exist, `toml_edit` is used to append the rule while preserving -/// existing formatting and comments. -/// -/// **Not concurrent-safe.** The read-modify-write cycle is not atomic. In the -/// current UI this is fine — the Select widget serializes permission decisions — -/// but callers should not invoke this concurrently for the same file. -pub(crate) async fn write_rule( - file_path: &Path, - rule: &Rule, - disposition: RuleDisposition, -) -> Result<()> { - let content = if tokio::fs::try_exists(file_path).await.unwrap_or(false) { - tokio::fs::read_to_string(file_path).await? - } else { - String::new() - }; - - let mut doc: toml_edit::DocumentMut = content.parse()?; - - // Ensure [permissions] table exists - if !doc.contains_key("permissions") { - doc["permissions"] = toml_edit::Item::Table(toml_edit::Table::new()); - } - - let key = match disposition { - RuleDisposition::Allow => "allow", - RuleDisposition::Deny => "deny", - }; - - // Use as_table_like_mut so both standard and inline tables work. - let permissions = doc["permissions"] - .as_table_like_mut() - .ok_or_else(|| eyre::eyre!("[permissions] is not a table"))?; - - // Get or create the array - if !permissions.contains_key(key) { - permissions.insert(key, toml_edit::Item::Value(toml_edit::Array::new().into())); - } - - let array = permissions - .get_mut(key) - .and_then(|item| item.as_value_mut()) - .and_then(|v| v.as_array_mut()) - .ok_or_else(|| eyre::eyre!("permissions.{key} is not an array"))?; - - // Don't add duplicates - let rule_str = rule.to_string(); - let already_present = array.iter().any(|v| v.as_str() == Some(&rule_str)); - if !already_present { - array.push(rule_str); - } - - // Write back, creating parent directories as needed - if let Some(parent) = file_path.parent() { - tokio::fs::create_dir_all(parent).await?; - } - tokio::fs::write(file_path, doc.to_string()).await?; - - Ok(()) -} - -/// Build the path to the project-level permissions file. -/// `project_root` is typically a git root or the current working directory. -pub(crate) fn project_permissions_path(project_root: &Path) -> std::path::PathBuf { - project_root.join(".atuin").join("permissions.ai.toml") -} - -/// Build the path to the global permissions file (sibling of atuin config). -pub(crate) fn global_permissions_path() -> std::path::PathBuf { - atuin_common::utils::config_dir().join("permissions.ai.toml") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn creates_new_file_with_allow_rule() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("permissions.ai.toml"); - let rule = Rule { - tool: "AtuinHistory".to_string(), - scope: None, - }; - - write_rule(&file, &rule, RuleDisposition::Allow) - .await - .unwrap(); - - let content = tokio::fs::read_to_string(&file).await.unwrap(); - assert!(content.contains("[permissions]")); - assert!(content.contains(r#""AtuinHistory""#)); - } - - #[tokio::test] - async fn appends_to_existing_file() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("permissions.ai.toml"); - let existing = r#"# My permissions -[permissions] -allow = ["Read"] -"#; - tokio::fs::write(&file, existing).await.unwrap(); - - let rule = Rule { - tool: "AtuinHistory".to_string(), - scope: None, - }; - write_rule(&file, &rule, RuleDisposition::Allow) - .await - .unwrap(); - - let content = tokio::fs::read_to_string(&file).await.unwrap(); - // Comment preserved - assert!(content.contains("# My permissions")); - // Both rules present - assert!(content.contains(r#""Read""#)); - assert!(content.contains(r#""AtuinHistory""#)); - } - - #[tokio::test] - async fn does_not_duplicate_existing_rule() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("permissions.ai.toml"); - let existing = r#"[permissions] -allow = ["AtuinHistory"] -"#; - tokio::fs::write(&file, existing).await.unwrap(); - - let rule = Rule { - tool: "AtuinHistory".to_string(), - scope: None, - }; - write_rule(&file, &rule, RuleDisposition::Allow) - .await - .unwrap(); - - let content = tokio::fs::read_to_string(&file).await.unwrap(); - // Should appear exactly once - assert_eq!(content.matches("AtuinHistory").count(), 1); - } - - #[tokio::test] - async fn handles_inline_table_permissions() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("permissions.ai.toml"); - // Inline table style — as_table_mut() would return None for this - let existing = r#"permissions = { allow = ["Read"] } -"#; - tokio::fs::write(&file, existing).await.unwrap(); - - let rule = Rule { - tool: "AtuinHistory".to_string(), - scope: None, - }; - write_rule(&file, &rule, RuleDisposition::Allow) - .await - .unwrap(); - - let content = tokio::fs::read_to_string(&file).await.unwrap(); - assert!(content.contains(r#""Read""#)); - assert!(content.contains(r#""AtuinHistory""#)); - } - - #[tokio::test] - async fn writes_deny_rule() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("permissions.ai.toml"); - let rule = Rule { - tool: "Shell".to_string(), - scope: None, - }; - - write_rule(&file, &rule, RuleDisposition::Deny) - .await - .unwrap(); - - let content = tokio::fs::read_to_string(&file).await.unwrap(); - assert!(content.contains("deny")); - assert!(content.contains(r#""Shell""#)); - } -} |
