aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/permissions/check.rs
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-04-10 13:24:57 -0700
committerGitHub <noreply@github.com>2026-04-10 20:24:57 +0000
commit09279a428659cf41824737d3e0c97bcc19a8885a (patch)
tree64731502c065df2483e8dd680d46c5559f3094f2 /crates/atuin-ai/src/permissions/check.rs
parentfeat: add strip_trailing_whitespace, on by default (#3390) (diff)
downloadatuin-09279a428659cf41824737d3e0c97bcc19a8885a.zip
feat: Client-tool execution + permission system (#3370)
Adds client-side tool execution to Atuin AI, starting with `atuin_history`. The server can request tool calls, which are executed locally with a permission system, and results are sent back to continue the conversation.
Diffstat (limited to 'crates/atuin-ai/src/permissions/check.rs')
-rw-r--r--crates/atuin-ai/src/permissions/check.rs74
1 files changed, 74 insertions, 0 deletions
diff --git a/crates/atuin-ai/src/permissions/check.rs b/crates/atuin-ai/src/permissions/check.rs
new file mode 100644
index 00000000..6b908b93
--- /dev/null
+++ b/crates/atuin-ai/src/permissions/check.rs
@@ -0,0 +1,74 @@
+use eyre::Result;
+
+use crate::{permissions::file::RuleFile, tools::PermissableToolCall};
+
+pub(crate) struct PermissionRequest<'t> {
+ call: &'t (dyn PermissableToolCall + Send + Sync),
+}
+
+impl<'t> PermissionRequest<'t> {
+ pub fn new(call: &'t (dyn PermissableToolCall + Send + Sync)) -> Self {
+ Self { call }
+ }
+}
+
+pub(crate) enum PermissionResponse {
+ Allowed,
+ Denied,
+ Ask,
+}
+
+pub(crate) struct PermissionChecker {
+ files: Vec<RuleFile>,
+}
+
+impl PermissionChecker {
+ pub fn new(files: Vec<RuleFile>) -> Self {
+ Self { files }
+ }
+
+ pub async fn check<'t>(
+ &self,
+ request: &'t PermissionRequest<'t>,
+ ) -> Result<PermissionResponse> {
+ // Files are in order from deepest to shallowest, so we can stop at the first match.
+ // Within a file, the priority is ask -> deny -> allow
+ // The first rule type that matches is the one that applies, even if a later rule would contradict it.
+ for file in &self.files {
+ for rule in &file.content.permissions.ask {
+ if request.call.matches_rule(rule) {
+ tracing::debug!(
+ "Permission 'ASK' by rule: {} in file: {}",
+ rule,
+ file.path.display()
+ );
+ return Ok(PermissionResponse::Ask);
+ }
+ }
+
+ for rule in &file.content.permissions.deny {
+ if request.call.matches_rule(rule) {
+ tracing::debug!(
+ "Permission 'DENY' by rule: {} in file: {}",
+ rule,
+ file.path.display()
+ );
+ return Ok(PermissionResponse::Denied);
+ }
+ }
+
+ for rule in &file.content.permissions.allow {
+ if request.call.matches_rule(rule) {
+ tracing::debug!(
+ "Permission 'ALLOW' by rule: {} in file: {}",
+ rule,
+ file.path.display()
+ );
+ return Ok(PermissionResponse::Allowed);
+ }
+ }
+ }
+
+ Ok(PermissionResponse::Ask)
+ }
+}