Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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.

73 changes: 73 additions & 0 deletions crates/agentflare-shim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ 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.
#[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| h == d) {
return false;
}
if d.join(PROJECT_MARKER).exists() {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 +165,53 @@ 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);
}

#[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