rig_core/tool/footprint.rs
1//! What a tool call may read and change ([`Touches`]), so a host runs the
2//! calls of one reply that touch different things side by side, and holds
3//! back only those that conflict.
4//!
5//! ```
6//! use std::path::PathBuf;
7//! use rig_core::tool::{Scope, Touches};
8//!
9//! let paths = |path: &str| Scope::Paths(vec![PathBuf::from(path)]);
10//! let edit = Touches::Writes(paths("/work/src/a.rs"));
11//! assert!(!edit.conflicts_with(&Touches::Reads(paths("/work/src/b.rs"))));
12//! assert!(edit.conflicts_with(&Touches::Reads(paths("/work/src"))));
13//! assert!(edit.conflicts_with(&Touches::Independent));
14//! assert!(!Touches::Reads(Scope::Anything).conflicts_with(&Touches::Independent));
15//! ```
16
17use std::path::{Component, PathBuf};
18
19use serde::{Deserialize, Serialize};
20
21use crate::completion::ToolAnnotations;
22
23/// What a tool call may read and change, so calls that touch different
24/// things run side by side.
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum Touches {
28 /// Reads what the scope names and changes nothing: run again after a
29 /// restart.
30 Reads(Scope),
31 /// Reads and may change what the scope names.
32 Writes(Scope),
33 /// Changes nothing the other calls of its reply see, such as handing
34 /// work to another agent. It may read anything, so it waits behind an
35 /// earlier writer; it is never run again after a restart.
36 Independent,
37}
38
39/// What a footprint names: these paths (absolute; a directory covers what
40/// is under it), or anything at all. A guard compares paths as they are,
41/// so a host makes a footprint [`normalized`](Touches::normalized) before
42/// any guard sees it; the rig kernel does so for every tool call.
43///
44/// Normalizing folds `.` and `..` by the text alone and follows no link,
45/// so a tool declares canonical paths: the files it will open once links
46/// are resolved, as rig-tools' file tools do through their workspace's
47/// `canonical`. A guard that must not be fooled by a link a tool did not
48/// resolve canonicalizes the paths it compares itself.
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum Scope {
52 /// These files and directories, canonical and absolute.
53 Paths(Vec<PathBuf>),
54 /// Anything at all.
55 Anything,
56}
57
58impl Scope {
59 /// Whether the two name something in common: a path is, or is under, a
60 /// path of the other, compared component by component; `Anything`
61 /// meets everything.
62 pub fn overlaps(&self, other: &Scope) -> bool {
63 match (self, other) {
64 (Self::Paths(ours), Self::Paths(theirs)) => ours.iter().any(|ours| {
65 theirs
66 .iter()
67 .any(|theirs| ours.starts_with(theirs) || theirs.starts_with(ours))
68 }),
69 (Self::Anything, _) | (_, Self::Anything) => true,
70 }
71 }
72
73 /// The scope with each path's `.` and `..` folded away, by the path's
74 /// text alone, so `/work/src/../../etc` names `/etc`, as a guard
75 /// comparing paths must see it. A relative path names no place a guard
76 /// can check, so a scope with one is [`Anything`](Self::Anything).
77 pub fn normalized(self) -> Scope {
78 match self {
79 Self::Paths(paths) => paths
80 .into_iter()
81 .map(normal)
82 .collect::<Option<Vec<_>>>()
83 .map_or(Self::Anything, Self::Paths),
84 Self::Anything => Self::Anything,
85 }
86 }
87}
88
89/// `path` with its `.` and `..` folded away, or `None` when it is relative.
90/// `..` at the root stays at the root.
91fn normal(path: PathBuf) -> Option<PathBuf> {
92 if !path.is_absolute() {
93 return None;
94 }
95 let mut normal = PathBuf::new();
96 for component in path.components() {
97 match component {
98 Component::ParentDir => {
99 normal.pop();
100 }
101 Component::CurDir => {}
102 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
103 normal.push(component);
104 }
105 }
106 }
107 Some(normal)
108}
109
110impl Touches {
111 /// Whether a call touching `self` must not run beside one touching
112 /// `other`: one of them writes what the other reads or writes.
113 /// [`Independent`](Self::Independent) counts as `Reads(Anything)` here.
114 pub fn conflicts_with(&self, other: &Touches) -> bool {
115 let (ours, we_write) = self.parts();
116 let (theirs, they_write) = other.parts();
117 (we_write || they_write) && ours.overlaps(theirs)
118 }
119
120 /// Whether it only reads: [`Reads`](Self::Reads), and nothing else.
121 pub fn reads_only(&self) -> bool {
122 matches!(self, Self::Reads(_))
123 }
124
125 /// The footprint with its scope [`normalized`](Scope::normalized).
126 pub fn normalized(self) -> Touches {
127 match self {
128 Self::Reads(scope) => Self::Reads(scope.normalized()),
129 Self::Writes(scope) => Self::Writes(scope.normalized()),
130 Self::Independent => Self::Independent,
131 }
132 }
133
134 /// From the annotations a tool's own code declares, such as a
135 /// [`Tool`](super::Tool)'s: `Reads(Anything)` when it says it only
136 /// reads, else `Writes(Anything)`. For annotations from elsewhere, such
137 /// as an MCP server's, [`from_hints`](Self::from_hints).
138 pub fn from_annotations(annotations: &ToolAnnotations) -> Self {
139 match annotations.read_only() {
140 true => Self::Reads(Scope::Anything),
141 false => Self::Writes(Scope::Anything),
142 }
143 }
144
145 /// From annotations rig cannot check, such as those an MCP server
146 /// sends: a tool that says it only reads is
147 /// [`Independent`](Self::Independent), so its calls still run beside
148 /// readers but are never run again after a restart; any other is
149 /// `Writes(Anything)`. Only a local declaration makes a call
150 /// [`Reads`](Self::Reads).
151 pub fn from_hints(annotations: &ToolAnnotations) -> Self {
152 match annotations.read_only() {
153 true => Self::Independent,
154 false => Self::Writes(Scope::Anything),
155 }
156 }
157
158 /// What it names, and whether it writes there.
159 fn parts(&self) -> (&Scope, bool) {
160 match self {
161 Self::Reads(scope) => (scope, false),
162 Self::Writes(scope) => (scope, true),
163 Self::Independent => (&Scope::Anything, false),
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests;