use std::path::{Path, PathBuf};
use trusty_mpm::core::project_aliases::main_checkout_root;
use super::pm_guard::{EDIT_TOOLS, edit_tool_target_path, is_source_code_path};
use super::pm_guard_bash::operator_checkouts::is_documents_repo;
use super::pm_guard_bash::{UnplaceableWrite, shell_write_targets};
pub(crate) fn evaluate_main_checkout_write(
tool_name: &str,
tool_input: Option<&serde_json::Value>,
cwd: &Path,
) -> Option<String> {
evaluate_main_checkout_write_with(tool_name, tool_input, cwd, home_dir().as_deref())
}
fn evaluate_main_checkout_write_with(
tool_name: &str,
tool_input: Option<&serde_json::Value>,
cwd: &Path,
home: Option<&str>,
) -> Option<String> {
let targets = match write_targets(tool_name, tool_input) {
Ok(targets) => targets,
Err(what) => return Some(unplaceable_write_reason(what)),
};
targets
.into_iter()
.find_map(|target| deny_reason_for_target(&target, cwd, home))
}
fn deny_reason_for_target(target: &str, cwd: &Path, home: Option<&str>) -> Option<String> {
let resolvable = expand_leading_base(target, cwd, home)?;
if base_directory_is_unresolvable(&resolvable) {
return None;
}
if !is_source_code_path(&resolvable) {
return None;
}
let resolved = resolve_write_target(&resolvable, cwd);
let root = main_checkout_root(&resolved)?;
if write_lands_in_a_scratchpad_clone(&resolved, &root) {
return None;
}
if is_documents_repo(&root) {
return None;
}
Some(deny_reason(target))
}
const SCRATCHPAD_SEGMENT: &str = "scratchpad";
pub(crate) fn write_lands_in_a_scratchpad_clone(resolved: &Path, root: &Path) -> bool {
let Ok(real_root) = root.canonicalize() else {
return false;
};
if scratchpad_root(&real_root).is_none() {
return false;
}
canonical_existing_ancestor(resolved).is_some_and(|dir| dir.starts_with(&real_root))
}
pub(crate) fn canonical_existing_ancestor(path: &Path) -> Option<PathBuf> {
path.ancestors().find_map(|dir| dir.canonicalize().ok())
}
pub(crate) fn scratchpad_root(path: &Path) -> Option<PathBuf> {
let mut prefix = PathBuf::new();
for component in path.components() {
prefix.push(component);
if component.as_os_str() == SCRATCHPAD_SEGMENT
&& trusty_common::bin_resolve::is_under_system_temp(&prefix)
{
return Some(prefix);
}
}
None
}
fn write_targets(
tool_name: &str,
tool_input: Option<&serde_json::Value>,
) -> Result<Vec<String>, UnplaceableWrite> {
if EDIT_TOOLS.contains(&tool_name) {
return Ok(edit_tool_target_path(tool_input)
.map(str::to_owned)
.into_iter()
.collect());
}
if tool_name != "Bash" {
return Ok(Vec::new());
}
let Some(command) = tool_input
.and_then(|v| v.get("command"))
.and_then(|v| v.as_str())
else {
return Ok(Vec::new());
};
shell_write_targets(command)
}
fn home_dir() -> Option<String> {
std::env::var("HOME").ok().filter(|home| !home.is_empty())
}
fn expand_leading_base(target: &str, cwd: &Path, home: Option<&str>) -> Option<String> {
if let Some(rest) = target.strip_prefix('~') {
if !(rest.is_empty() || rest.starts_with('/')) {
return None;
}
return Some(join_base(home?, rest));
}
let (first, rest) = match target.split_once('/') {
Some((first, rest)) => (first, format!("/{rest}")),
None => (target, String::new()),
};
match first {
"$PWD" | "${PWD}" => Some(join_base(&cwd.to_string_lossy(), &rest)),
"$HOME" | "${HOME}" => Some(join_base(home?, &rest)),
_ => Some(target.to_string()),
}
}
fn join_base(base: &str, rest: &str) -> String {
format!("{}{rest}", base.trim_end_matches('/'))
}
fn base_directory_is_unresolvable(target: &str) -> bool {
let Some((directory, _file)) = target.rsplit_once('/') else {
return names_an_expansion(target);
};
directory
.split('/')
.find(|segment| !segment.is_empty())
.is_some_and(names_an_expansion)
}
fn names_an_expansion(segment: &str) -> bool {
segment.contains('$') || segment.contains('`')
}
fn resolve_write_target(target: &str, cwd: &Path) -> PathBuf {
let path = Path::new(target);
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
};
absolute
.parent()
.map_or_else(|| absolute.clone(), Path::to_path_buf)
}
fn deny_reason(target: &str) -> String {
format!(
"Source write denied in a main checkout (ADR-0044): `{target}` is a source file in a \
project's main checkout, which is read-only apart from documents and configuration. \
Other sessions stand in this same directory — the reported failure is branches \
switching under each other and a commit landing on a workstream it did not belong to, \
with no error at any step. Do this work in a worktree instead: if you are a dispatched \
agent, ask the PM to re-dispatch you with `isolation: \"worktree\"`, which gets you \
your own tree; if you are the PM, dispatch the change to an agent rather than writing \
it here. Documents (`.md`), configuration (`.toml`, `.json`, `.yaml`), and everything \
under `.claude/worktrees/**` stay writable and are not affected by this rule."
)
}
fn unplaceable_write_reason(what: UnplaceableWrite) -> String {
format!(
"Write target could not be placed (ADR-0044, #8730): this command writes through {what}, \
so the guard cannot tell whether the write lands in a project's main checkout, and it \
refuses rather than guess. Close the quote or parenthesis (bash rejects the command as \
written), or flatten the nesting, and run it again."
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn main_checkout() -> TempDir {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir(dir.path().join(".git")).expect("mkdir .git");
dir
}
fn write_input(path: &Path) -> serde_json::Value {
serde_json::json!({"file_path": path.to_string_lossy(), "content": "fn main() {}"})
}
#[test]
fn denies_a_source_write_in_a_main_checkout() {
let dir = main_checkout();
for tool in EDIT_TOOLS {
let reason = evaluate_main_checkout_write(
tool,
Some(&write_input(&dir.path().join("src/lib.rs"))),
dir.path(),
)
.unwrap_or_else(|| panic!("{tool} on a source file in a main checkout must be denied"));
assert!(reason.contains("ADR-0044"), "{reason}");
}
}
#[test]
fn allows_documents_and_configuration() {
let dir = main_checkout();
for name in [
"README.md",
"CLAUDE.md",
"Cargo.toml",
".claude/settings.json",
"TASK.md",
"docs/adr/0044-x.md",
"Makefile",
] {
assert_eq!(
evaluate_main_checkout_write(
"Write",
Some(&write_input(&dir.path().join(name))),
dir.path()
),
None,
"{name} is a document or configuration and must stay writable"
);
}
}
#[test]
fn allows_a_write_inside_a_worktree() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join(".git"), "gitdir: /elsewhere").expect("write .git");
assert_eq!(
evaluate_main_checkout_write(
"Write",
Some(&write_input(&dir.path().join("src/lib.rs"))),
dir.path()
),
None
);
}
#[test]
fn allows_a_write_outside_any_repository() {
let dir = tempfile::tempdir().expect("tempdir");
assert_eq!(
evaluate_main_checkout_write(
"Write",
Some(&write_input(&dir.path().join("scratch.rs"))),
dir.path()
),
None
);
}
#[test]
fn allows_every_tool_that_names_no_write() {
let dir = main_checkout();
for tool in ["Read", "Bash", "Grep", "Agent", "SendMessage"] {
assert_eq!(
evaluate_main_checkout_write(
tool,
Some(&write_input(&dir.path().join("src/lib.rs"))),
dir.path()
),
None,
"{tool} names no write here"
);
}
}
fn bash_input(command: &str) -> serde_json::Value {
serde_json::json!({"command": command})
}
#[test]
fn denies_a_git_output_write_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs");
let target = target.display().to_string();
for command in [
format!("git diff --output={target} HEAD~1 HEAD"),
format!("git diff --output {target} HEAD"),
format!("git log -1 --output={target}"),
format!("git show HEAD --output={target}"),
format!("git -C {} diff --output={target}", dir.path().display()),
] {
let reason =
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path())
.unwrap_or_else(|| {
panic!("`{command}` writes into a main checkout and must deny")
});
assert!(reason.contains("ADR-0044"), "{reason}");
}
}
#[test]
fn denies_a_shell_redirect_into_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs");
let command = format!("echo 'fn main() {{}}' > {}", target.display());
let reason = evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path())
.expect("a redirect into a main checkout's source must deny");
assert!(reason.contains("ADR-0044"), "{reason}");
}
#[test]
fn denies_a_second_segments_source_write_when_the_first_writes_elsewhere() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs");
let command = format!(
"echo hi > {}/notes.md && echo 'fn main() {{}}' > {}",
dir.path().display(),
target.display()
);
let reason = evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path())
.unwrap_or_else(|| {
panic!("`{command}`'s second segment writes source into the checkout")
});
assert!(reason.contains("ADR-0044"), "{reason}");
}
#[test]
fn denies_a_cd_segments_own_redirect_in_a_main_checkout() {
let dir = main_checkout();
let command = "cd . > crates/x/src/lib.rs";
let reason = evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir.path())
.expect("a `cd` segment's own redirect into a main checkout's source must deny");
assert!(reason.contains("ADR-0044"), "{reason}");
}
#[test]
fn denies_a_later_segments_write_resolved_against_the_hook_cwd_not_a_preceding_cd() {
let dir = main_checkout();
let command = "cd /tmp && echo 'fn main() {}' > crates/x/src/lib.rs";
let reason = evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir.path())
.expect("the relative target must resolve against the hook cwd, not /tmp");
assert!(reason.contains("ADR-0044"), "{reason}");
}
fn assert_each_denies(dir: &Path, commands: &[String]) {
for command in commands {
let reason = evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir)
.unwrap_or_else(|| panic!("`{command}` writes source into a main checkout"));
assert!(reason.contains("ADR-0044"), "{reason}");
}
}
#[test]
fn denies_a_tee_write_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs").display().to_string();
assert_each_denies(
dir.path(),
&[
format!("tee {target}"),
format!("echo x | tee {target}"),
format!("echo x | tee -a {target} > /dev/null"),
format!("echo x | sudo tee -- {target}"),
],
);
}
#[test]
fn denies_a_command_substitution_write_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs").display().to_string();
assert_each_denies(
dir.path(),
&[
format!("echo $(echo x > {target})"),
format!("echo \"$(echo x > {target})\""),
format!("echo `echo x > {target}`"),
format!("echo $(echo x | tee {target})"),
format!("echo x > >(tee {target})"),
],
);
}
#[test]
fn denies_a_subshell_write_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs").display().to_string();
assert_each_denies(
dir.path(),
&[
format!("(echo x > {target})"),
format!("( cd /tmp && echo x > {target} )"),
format!("(echo x | tee {target})"),
],
);
}
#[test]
fn denies_a_clobber_redirect_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs").display().to_string();
assert_each_denies(
dir.path(),
&[
format!("echo x >|{target}"),
format!("echo x >| {target}"),
format!("echo x >>| {target}"),
format!("echo x >! {target}"),
format!("echo $(echo x >| {target})"),
],
);
}
#[test]
fn denies_a_descriptor_redirect_that_names_a_file_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs").display().to_string();
assert_each_denies(
dir.path(),
&[
format!("echo x >&{target}"),
format!("echo x >& {target}"),
format!("echo x >&!{target}"),
format!("echo x >&| {target}"),
],
);
let benign = "cargo test 2>&1 >&2";
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(benign)), dir.path()),
None
);
}
#[test]
fn denies_a_substitution_inside_a_shell_heredoc_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs").display().to_string();
assert_each_denies(
dir.path(),
&[
format!("bash <<'EOF'\n$(tee {target})\nEOF"),
format!("bash <<'EOF'\necho $(echo x | tee {target})\nEOF"),
format!("sh <<'EOF'\nsh -c \"$(echo x > {target})\"\nEOF"),
format!("bash <<'EOF'\necho x > {target}\nEOF"),
format!("bash <<'EOF'\necho x | tee {target}\nEOF"),
],
);
let pad = tempfile::tempdir().expect("tempdir");
let pad = pad.path().join("scratchpad");
std::fs::create_dir_all(&pad).expect("mkdir scratchpad");
let data = format!(
"cat <<'EOF' > {}\n$(tee {target})\nEOF",
pad.join("x.md").display()
);
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&data)), dir.path()),
None,
"a data here-document quoting a write is not a write"
);
}
#[test]
fn denies_a_write_whose_target_cannot_be_placed() {
let dir = tempfile::tempdir().expect("tempdir");
for command in [
"echo $(echo x > src/lib.rs",
"(echo x > src/lib.rs",
"echo `echo x > src/lib.rs",
"echo x | tee 'src/lib.rs",
] {
let reason =
evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir.path())
.unwrap_or_else(|| panic!("`{command}` cannot be placed and must deny"));
assert!(reason.contains("#8730"), "{reason}");
}
}
#[test]
fn allows_tee_substitution_and_subshell_writes_that_are_not_source_in_a_checkout() {
let worktree = tempfile::tempdir().expect("tempdir");
std::fs::write(worktree.path().join(".git"), "gitdir: /elsewhere").expect("write .git");
let main = main_checkout();
let in_worktree = worktree
.path()
.join("crates/x/src/lib.rs")
.display()
.to_string();
let notes = main.path().join("notes.md").display().to_string();
let source = main
.path()
.join("crates/x/src/lib.rs")
.display()
.to_string();
for command in [
format!("echo x | tee {in_worktree}"),
format!("echo $(echo x > {in_worktree})"),
format!("(echo x > {in_worktree})"),
format!("echo x | tee -a {notes}"),
format!("(echo x > {notes})"),
format!("echo '$(echo x > {source})'"),
format!("grep -n tee {source}"),
] {
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), main.path()),
None,
"`{command}` writes no source into a main checkout"
);
}
}
#[test]
fn allows_a_scratchpad_heredoc_whose_body_names_a_source_write() {
let main = main_checkout();
let pad = tempfile::tempdir().expect("tempdir");
let pad = pad.path().join("scratchpad");
std::fs::create_dir_all(&pad).expect("mkdir scratchpad");
let source = main
.path()
.join("crates/x/src/lib.rs")
.display()
.to_string();
let body = format!(
"see: echo x > {source}\nrun $(echo x > {source}), (echo x > {source}) or \
`tee {source}`;\nit's done"
);
for name in ["report.md", "probe.rs"] {
let command = format!("cat > {} <<'EOF'\n{body}\nEOF", pad.join(name).display());
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), main.path()),
None,
"`{command}` writes only the scratchpad"
);
}
assert_each_denies(
main.path(),
&[
format!("cat > {source} <<'EOF'\nfn main() {{}}\nEOF"),
format!("echo x | tee {source} <<'EOF'\nfn main() {{}}\nEOF"),
format!("cat > {}/r.md; echo x | tee {source}", pad.display()),
],
);
}
#[test]
fn allows_a_git_output_write_inside_a_worktree() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join(".git"), "gitdir: /elsewhere").expect("write .git");
let target = dir.path().join("crates/x/src/lib.rs");
let command = format!("git diff --output={} HEAD", target.display());
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path()),
None
);
}
#[test]
fn allows_a_git_read_in_a_main_checkout() {
let dir = main_checkout();
let a = dir.path().join("crates/x/src/lib.rs");
let a = a.display().to_string();
for command in [
format!("git diff --no-index {a} {a}"),
"git diff HEAD~1 HEAD".to_string(),
"git diff --stat".to_string(),
format!("git diff --output-indicator-new=+ -- {a}"),
format!("git format-patch --stdout -1 HEAD -- {a}"),
format!("sed -n '1,5p' {a}"),
] {
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path()),
None,
"`{command}` writes nothing"
);
}
}
#[test]
fn allows_a_heredoc_body_redirect_in_a_main_checkout() {
let dir = main_checkout();
let target = dir.path().join("crates/x/src/lib.rs");
let command = format!("cat <<'EOF'\nsee: git diff > {}\nEOF", target.display());
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path()),
None,
"a here-document body is data, not a write"
);
let live = format!("python3 <<'PY' > {}\nprint(1)\nPY", target.display());
assert!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&live)), dir.path()).is_some(),
"a redirect on the operator line is still a write"
);
}
#[test]
fn allows_a_git_output_write_of_a_document() {
let dir = main_checkout();
let command = format!("git diff --output={}/notes.md HEAD", dir.path().display());
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path()),
None
);
}
#[test]
fn allows_a_call_with_no_readable_target() {
let dir = main_checkout();
for input in [
None,
Some(serde_json::json!({})),
Some(serde_json::json!({"file_path": ""})),
Some(serde_json::json!({"file_path": 7})),
] {
assert_eq!(
evaluate_main_checkout_write("Write", input.as_ref(), dir.path()),
None,
"{input:?} names no target"
);
}
}
#[test]
fn resolves_a_relative_target_against_the_hook_cwd() {
let dir = main_checkout();
assert!(
evaluate_main_checkout_write(
"Edit",
Some(&serde_json::json!({"file_path": "src/lib.rs"})),
dir.path()
)
.is_some(),
"a relative source path must resolve against the hook cwd"
);
}
#[test]
fn denies_an_absolute_write_from_outside_the_checkout() {
let checkout = main_checkout();
let elsewhere = tempfile::tempdir().expect("tempdir");
assert!(
evaluate_main_checkout_write(
"Write",
Some(&write_input(&checkout.path().join("src/lib.rs"))),
elsewhere.path()
)
.is_some(),
"the target decides, not the caller's directory"
);
}
#[test]
fn allows_a_target_whose_base_directory_is_an_unexpanded_expansion() {
let dir = main_checkout();
for command in [
"cat <<'EOF' > $SP/v1.zsh\necho hi\nEOF",
"echo hi > ${SP}/v1.zsh",
"echo hi > $(mktemp -d)/probe.rs",
"echo hi > $SCRATCH.rs",
] {
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir.path()),
None,
"`{command}` names no base directory this guard can resolve"
);
}
assert_eq!(
evaluate_main_checkout_write(
"Write",
Some(&serde_json::json!({"file_path": "$SP/v1.zsh"})),
dir.path()
),
None,
);
}
#[test]
fn denies_an_expansion_confined_to_the_filename() {
let dir = main_checkout();
for command in [
"echo hi > crates/x/src/$(true)lib.rs",
"echo hi > crates/x/src/`true`lib.rs",
] {
let reason =
evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir.path())
.unwrap_or_else(|| {
panic!("`{command}` lands in a literal in-checkout directory")
});
assert!(reason.contains("ADR-0044"), "{reason}");
}
}
#[test]
fn denies_an_expansion_in_a_middle_segment() {
let dir = main_checkout();
for input in [
serde_json::json!({"file_path": "crates/$(echo trusty-mpm)/src/lib.rs"}),
serde_json::json!({"file_path": "crates/${PKG}/src/lib.rs"}),
] {
let reason = evaluate_main_checkout_write("Write", Some(&input), dir.path())
.unwrap_or_else(|| panic!("{input} is still relative to the hook cwd"));
assert!(reason.contains("ADR-0044"), "{reason}");
}
let command = "echo hi > crates/${PKG}/src/lib.rs";
let reason = evaluate_main_checkout_write("Bash", Some(&bash_input(command)), dir.path())
.expect("a relative path under the hook cwd is still in the checkout");
assert!(reason.contains("ADR-0044"), "{reason}");
}
#[test]
fn denies_a_leading_tilde_that_resolves_into_the_checkout() {
let home = tempfile::tempdir().expect("tempdir");
let checkout = home.path().join("proj");
std::fs::create_dir_all(checkout.join(".git")).expect("mkdir .git");
let elsewhere = tempfile::tempdir().expect("tempdir");
let home = home.path().to_string_lossy().into_owned();
let reason = evaluate_main_checkout_write_with(
"Write",
Some(&serde_json::json!({"file_path": "~/proj/src/lib.rs"})),
elsewhere.path(),
Some(&home),
)
.expect("a tilde path resolving into a main checkout must deny");
assert!(reason.contains("ADR-0044"), "{reason}");
assert!(reason.contains("~/proj/src/lib.rs"), "{reason}");
}
#[test]
fn allows_a_leading_tilde_when_home_is_unknown() {
let dir = main_checkout();
assert_eq!(
evaluate_main_checkout_write_with(
"Write",
Some(&serde_json::json!({"file_path": "~/proj/src/lib.rs"})),
dir.path(),
None,
),
None,
"HOME unset leaves the tilde unresolvable"
);
let cwd = dir.path();
assert_eq!(
expand_leading_base("~someone/src/lib.rs", cwd, Some("/home/me")),
None
);
assert_eq!(
expand_leading_base("$HOME/src/lib.rs", cwd, None),
None,
"HOME unset leaves `$HOME` unresolvable too"
);
assert_eq!(
expand_leading_base("/tmp/a~b.rs", cwd, None),
Some("/tmp/a~b.rs".to_string()),
"a tilde that is not leading is an ordinary character"
);
for target in ["$PWDX/src/lib.rs", "$HOMEDIR/src/lib.rs", "$SP/v1.zsh"] {
assert_eq!(
expand_leading_base(target, cwd, Some("/home/me")),
Some(target.to_string()),
"{target} is not one of the four resolvable spellings"
);
}
}
#[test]
fn denies_a_dollar_pwd_first_segment_that_resolves_into_the_checkout() {
let dir = main_checkout();
let inputs = [
("Bash", bash_input("echo hi > $PWD/crates/x/src/lib.rs")),
(
"Write",
serde_json::json!({"file_path": "$PWD/crates/x/src/lib.rs"}),
),
(
"Edit",
serde_json::json!({"file_path": "${PWD}/crates/x/src/lib.rs"}),
),
];
for (tool, input) in inputs {
let reason = evaluate_main_checkout_write_with(tool, Some(&input), dir.path(), None)
.unwrap_or_else(|| panic!("{tool} {input} resolves to the hook cwd"));
assert!(reason.contains("ADR-0044"), "{reason}");
}
}
#[test]
fn denies_a_dollar_home_first_segment_that_resolves_into_the_checkout() {
let home = tempfile::tempdir().expect("tempdir");
let checkout = home.path().join("proj");
std::fs::create_dir_all(checkout.join(".git")).expect("mkdir .git");
let elsewhere = tempfile::tempdir().expect("tempdir");
let home = home.path().to_string_lossy().into_owned();
for spelling in ["$HOME", "${HOME}"] {
let target = format!("{spelling}/proj/src/lib.rs");
let reason = evaluate_main_checkout_write_with(
"Write",
Some(&serde_json::json!({ "file_path": target })),
elsewhere.path(),
Some(&home),
)
.unwrap_or_else(|| panic!("{target} resolves into a main checkout"));
assert!(reason.contains("ADR-0044"), "{reason}");
assert!(reason.contains(&target), "{reason}");
}
}
#[test]
fn a_literal_target_is_not_read_as_an_expansion() {
assert!(!base_directory_is_unresolvable("src/lib.rs"));
assert!(!base_directory_is_unresolvable("/tmp/a~b.rs"));
assert!(!base_directory_is_unresolvable(
"crates/x/src/$(true)lib.rs"
));
assert!(!base_directory_is_unresolvable(
"crates/$(echo x)/src/lib.rs"
));
assert!(base_directory_is_unresolvable("$SP/v1.zsh"));
assert!(base_directory_is_unresolvable("/$(echo tmp)/v1.zsh"));
let dir = main_checkout();
assert!(
evaluate_main_checkout_write(
"Write",
Some(&serde_json::json!({"file_path": "src/lib.rs"})),
dir.path()
)
.is_some(),
"a literal relative source path must still deny"
);
}
fn scratchpad_clone(name: &str) -> (TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let clone = dir.path().join("scratchpad").join(name);
std::fs::create_dir_all(clone.join(".git")).expect("mkdir .git");
(dir, clone)
}
#[test]
fn allows_a_source_write_in_a_scratchpad_rooted_clone() {
let (_dir, clone) = scratchpad_clone("revert-probe");
for name in ["crates/x/src/lib.rs", "probe.py"] {
assert_eq!(
evaluate_main_checkout_write(
"Write",
Some(&write_input(&clone.join(name))),
&clone
),
None,
"{name} lands in a disposable scratchpad clone, not a shared tree"
);
}
let command = format!(
"echo 'fn main() {{}}' > {}",
clone.join("probe.rs").display()
);
assert_eq!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), &clone),
None
);
}
#[test]
fn denies_a_checkout_reached_through_a_symlink_in_the_scratchpad() {
let dir = tempfile::tempdir().expect("tempdir");
let real = dir.path().join("realrepo");
std::fs::create_dir_all(real.join(".git")).expect("mkdir .git");
let pad = dir.path().join("scratchpad");
std::fs::create_dir_all(&pad).expect("mkdir scratchpad");
let link = pad.join("linkrepo");
std::os::unix::fs::symlink(&real, &link).expect("symlink");
let target = link.join("crates/trusty-mpm/src/zzz_probe.rs");
let reason = evaluate_main_checkout_write("Write", Some(&write_input(&target)), dir.path())
.expect("a symlink into a real checkout must not buy an exemption");
assert!(reason.contains("ADR-0044"), "{reason}");
let command = format!("echo 'fn main() {{}}' > {}", target.display());
assert!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), dir.path()).is_some(),
"the shell half must answer the same way"
);
}
#[test]
fn denies_a_dotdot_escape_from_the_scratchpad() {
let dir = tempfile::tempdir().expect("tempdir");
let real = dir.path().join("realrepo");
std::fs::create_dir_all(real.join(".git")).expect("mkdir .git");
let clone = dir.path().join("scratchpad").join("revert-probe");
std::fs::create_dir_all(clone.join(".git")).expect("mkdir .git");
let target = clone.join("../../realrepo/src/zzz_probe.rs");
let reason = evaluate_main_checkout_write("Write", Some(&write_input(&target)), dir.path())
.expect("a `..` escape into a real checkout must not buy an exemption");
assert!(reason.contains("ADR-0044"), "{reason}");
}
#[test]
fn denies_a_symlink_below_an_exempt_root() {
let dir = tempfile::tempdir().expect("tempdir");
let real = dir.path().join("realrepo");
std::fs::create_dir_all(real.join(".git")).expect("mkdir .git");
let real_src = real.join("crates/trusty-mpm/src");
std::fs::create_dir_all(&real_src).expect("mkdir src");
let clone = dir.path().join("scratchpad").join("revert-probe");
std::fs::create_dir_all(clone.join(".git")).expect("mkdir .git");
let link = clone.join("srclink");
std::os::unix::fs::symlink(&real_src, &link).expect("symlink");
let target = link.join("zzz_probe.rs");
let reason = evaluate_main_checkout_write("Write", Some(&write_input(&target)), &clone)
.expect("a symlink below an exempt root must not carry the write out");
assert!(reason.contains("ADR-0044"), "{reason}");
let command = format!("echo 'fn main() {{}}' > {}", target.display());
assert!(
evaluate_main_checkout_write("Bash", Some(&bash_input(&command)), &clone).is_some(),
"the shell half must answer the same way"
);
}
#[test]
fn allows_a_dotdot_that_lands_back_inside_the_exempt_clone() {
let (_dir, clone) = scratchpad_clone("revert-probe");
std::fs::create_dir_all(clone.join("crates")).expect("mkdir crates");
let target = clone.join("crates/../src/lib.rs");
assert_eq!(
evaluate_main_checkout_write("Write", Some(&write_input(&target)), &clone),
None,
"the write lands inside the exempt clone"
);
}
#[test]
fn denies_a_checkout_whose_path_merely_contains_the_scratchpad_name() {
let dir = main_checkout();
for name in ["scratchpad/probe.rs", "scratchpad/nested/probe.py"] {
let reason = evaluate_main_checkout_write(
"Write",
Some(&write_input(&dir.path().join(name))),
dir.path(),
)
.unwrap_or_else(|| panic!("{name} is inside a real checkout and must be refused"));
assert!(reason.contains("ADR-0044"), "{reason}");
}
}
#[test]
fn denies_when_no_scratchpad_root_can_be_determined() {
let dir = tempfile::tempdir().expect("tempdir");
let clone = dir.path().join("revert-probe");
std::fs::create_dir_all(clone.join(".git")).expect("mkdir .git");
assert_eq!(scratchpad_root(&clone), None);
assert!(
evaluate_main_checkout_write(
"Write",
Some(&write_input(&clone.join("probe.rs"))),
&clone
)
.is_some(),
"no scratchpad root could be determined, so the deny must stand"
);
}
#[test]
fn scratchpad_root_names_only_a_temp_rooted_harness_directory() {
assert_eq!(
scratchpad_root(Path::new(
"/private/tmp/claude-502/proj/9f1c/scratchpad/clone/src"
)),
Some(PathBuf::from(
"/private/tmp/claude-502/proj/9f1c/scratchpad"
))
);
assert_eq!(
scratchpad_root(Path::new("/Users/x/repos/scratchpad/clone")),
None
);
assert_eq!(
scratchpad_root(Path::new("/private/tmp/claude-502/scratchpad-notes/clone")),
None
);
assert_eq!(
scratchpad_root(Path::new("/private/tmp/claude-502/x")),
None
);
}
#[test]
fn deny_reason_names_the_file_and_both_remedies() {
let reason = deny_reason("src/lib.rs");
assert!(reason.contains("src/lib.rs"), "{reason}");
assert!(reason.contains(r#"isolation: "worktree""#), "{reason}");
assert!(reason.contains("Documents"), "{reason}");
}
}