Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions crates/agentflare-shim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ pub fn is_set(name: &str) -> bool {
env::var_os(name).is_some_and(|v| !v.is_empty())
}

/// Project marker directory: its presence means agentflare actually tracks
/// this project. Shared by every shim binary that needs to scope its
/// behavior to agentflare-managed projects only.
pub const PROJECT_MARKER: &str = ".agentflare";

/// Walk up from `start` looking for `.agentflare`, stopping at `home`
/// (exclusive) -- `~/.agentflare` is agentflare's own data dir, not a
/// project marker, and would otherwise false-positive on everything
/// under the user's home directory. Uses `paths_eq` rather than plain `==`
/// for the boundary check: a byte-equal comparison can miss the real match
/// when the ambient home dir and the walked-up ancestor differ only by case
/// or separator style (observed live: a real ambient `dirs::home_dir()` on a
/// Windows CI runner didn't byte-match the walk's own ancestor path, so the
/// boundary silently never triggered and the walk kept climbing).
#[must_use]
pub fn in_scoped_project(start: &Path, home: Option<&Path>) -> bool {
let mut dir = Some(start);
while let Some(d) = dir {
if home.is_some_and(|h| paths_eq(h, d)) {
return false;
}
if d.join(PROJECT_MARKER).exists() {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return true;
}
dir = d.parent();
}
false
}

/// Emits a trace line to stderr when `AGENTFLARE_SHIM_TRACE` is set.
pub fn trace(msg: &str) {
if is_set("AGENTFLARE_SHIM_TRACE") {
Expand Down Expand Up @@ -141,4 +170,69 @@ mod tests {
let b = Path::new("C:/Users/shiva/.agentflare/shims");
assert!(paths_eq(a, b), "/ vs \\ differences must match on Windows");
}

#[test]
fn finds_marker_in_start_dir() {
let tmp = std::env::temp_dir().join(format!("agentflare-shim-test-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
std::fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
assert!(in_scoped_project(&tmp, None));
let _ = std::fs::remove_dir_all(&tmp);
}

#[test]
fn finds_marker_in_an_ancestor_dir() {
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-anc-{}", std::process::id()));
let sub = tmp.join("a").join("b");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
assert!(in_scoped_project(&sub, None));
let _ = std::fs::remove_dir_all(&tmp);
}

#[test]
fn stops_at_home_without_treating_agentflares_own_dir_as_a_project_marker() {
// ~/.agentflare is agentflare's own app-data dir, not a project
// marker -- walking past `home` (inclusive of home itself) must
// never false-positive on it. Regression for the bug the doc
// comment on `in_scoped_project` calls out.
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-home-{}", std::process::id()));
let sub = tmp.join("sub");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
assert!(!in_scoped_project(&sub, Some(&tmp)));
let _ = std::fs::remove_dir_all(&tmp);
}

#[cfg(any(windows, target_os = "macos"))]
#[test]
fn stops_at_home_even_when_it_only_case_matches_the_walked_ancestor() {
// Regression for the exact bug this fix closes: a real ambient home
// dir on a Windows CI runner didn't byte-match the walk's own
// ancestor path, so the `home` boundary never triggered.
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-case-{}", std::process::id()));
let sub = tmp.join("sub");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
let uppercased_home = Path::new(&tmp.to_string_lossy().to_uppercase()).to_path_buf();
assert!(!in_scoped_project(&sub, Some(&uppercased_home)));
let _ = std::fs::remove_dir_all(&tmp);
}

#[test]
fn no_marker_anywhere_is_not_scoped() {
// Bound the walk-up with an explicit synthetic `home` one level
// above `tmp`, rather than `None` -- an unbounded walk from a real
// temp dir keeps climbing past this test's control (e.g. up into
// the real machine's actual `~/.agentflare`, giving a false pass/fail
// that has nothing to do with the logic under test).
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-none-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
assert!(!in_scoped_project(&tmp, tmp.parent()));
let _ = std::fs::remove_dir_all(&tmp);
}
}
79 changes: 4 additions & 75 deletions crates/agentflare-shim/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::{Command, exit};

use agentflare_shim::{is_set, path_without_shim_dir, run_real, tool_name_from_exe, trace};
use agentflare_shim::{
in_scoped_project, is_set, path_without_shim_dir, run_real, tool_name_from_exe, trace,
};

const KILL_SWITCHES: &[&str] = &["LEAN_CTX_DISABLED", "LEAN_CTX_NO_HOOK"];

Expand All @@ -43,30 +45,10 @@ const AGENT_ENV_VARS: &[&str] = &[
"CODEBUDDY",
];

const PROJECT_MARKER: &str = ".agentflare";

fn any_set(names: &[&str]) -> bool {
names.iter().any(|n| is_set(n))
}

/// Walk up from `start` looking for `.agentflare`, stopping at `home`
/// (exclusive) -- `~/.agentflare` is agentflare's own data dir, not a
/// project marker, and would otherwise false-positive on everything
/// under the user's home directory.
fn in_scoped_project(start: &Path, home: Option<&Path>) -> bool {
let mut dir = Some(start);
while let Some(d) = dir {
if home.is_some_and(|h| h == d) {
return false;
}
if d.join(PROJECT_MARKER).exists() {
return true;
}
dir = d.parent();
}
false
}

fn main() {
let exe = match env::current_exe() {
Ok(p) => p,
Expand Down Expand Up @@ -110,57 +92,4 @@ fn main() {
}
}

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

#[test]
fn finds_marker_in_start_dir() {
let tmp = std::env::temp_dir().join(format!("agentflare-shim-test-{}", std::process::id()));
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
assert!(in_scoped_project(&tmp, None));
let _ = fs::remove_dir_all(&tmp);
}

#[test]
fn finds_marker_in_an_ancestor_dir() {
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-anc-{}", std::process::id()));
let sub = tmp.join("a").join("b");
fs::create_dir_all(&sub).unwrap();
fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
assert!(in_scoped_project(&sub, None));
let _ = fs::remove_dir_all(&tmp);
}

#[test]
fn stops_at_home_without_treating_agentflares_own_dir_as_a_project_marker() {
// ~/.agentflare is agentflare's own app-data dir, not a project
// marker -- walking past `home` (inclusive of home itself) must
// never false-positive on it. Regression for the bug the doc
// comment on `in_scoped_project` calls out.
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-home-{}", std::process::id()));
let sub = tmp.join("sub");
fs::create_dir_all(&sub).unwrap();
fs::write(tmp.join(PROJECT_MARKER), "").unwrap();
assert!(!in_scoped_project(&sub, Some(&tmp)));
let _ = fs::remove_dir_all(&tmp);
}

#[test]
fn no_marker_anywhere_is_not_scoped() {
// Bound the walk-up with an explicit synthetic `home` one level
// above `tmp`, rather than `None` -- an unbounded walk from a real
// temp dir keeps climbing past this test's control (e.g. up into
// the real machine's actual `~/.agentflare`, giving a false pass/fail
// that has nothing to do with the logic under test).
let tmp =
std::env::temp_dir().join(format!("agentflare-shim-test-none-{}", std::process::id()));
fs::create_dir_all(&tmp).unwrap();
assert!(!in_scoped_project(&tmp, tmp.parent()));
let _ = fs::remove_dir_all(&tmp);
}
}
// `in_scoped_project` and its tests moved to lib.rs (shared with flare-git-core).
1 change: 1 addition & 0 deletions crates/flare-git-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ serde_json = "1"
chrono = "0.4"
rusqlite = { version = "0.40", features = ["bundled"] }
agentflare-backend = { package = "agentflare-backend", path = "../agentflare-backend" }
agentflare-shim = { path = "../agentflare-shim" }
walkdir = "2"
dirs = "6"
agent-detector = "0.2.1"
Expand Down
Loading
Loading