aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/permissions/writer.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/atuin-ai/src/permissions/writer.rs')
-rw-r--r--crates/atuin-ai/src/permissions/writer.rs199
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""#));
- }
-}