From 0156b1e61f675308a7019ead7e33f3767d6f2161 Mon Sep 17 00:00:00 2001 From: evawong-oai Date: Tue, 28 Apr 2026 09:10:41 -0700 Subject: [PATCH] [sandbox] Enforce protected workspace metadata paths (#19846) ## Summary Make FileSystemSandboxPolicy the semantic source of truth for project root metadata protection. Under writable roots, `.git`, `.codex`, and `.agents` stay protected unless user policy grants an explicit write rule for that metadata path. ## Scope 1. Add `protected_metadata_names` to `WritableRoot`. 2. Teach `FileSystemSandboxPolicy::can_write_path_with_cwd` to reject protected metadata writes under writable roots unless explicitly allowed. 3. Default workspace write profiles to protect `.git`, `.codex`, and `.agents`. 4. Add the Linux fallback setup needed before Linux enforcement lands later in the stack. ## Reviewer Focus 1. The policy decision belongs in FileSystemSandboxPolicy, not shell command parsing. 2. Legacy SandboxPolicy remains a compatibility projection, not the source of the new rule. 3. Explicit user write rules can still opt into these metadata paths. ## Stack 1. Policy primitive: this PR 2. macOS Seatbelt adapter: #19847 3. Shell preflight UX: #19848 4. Runtime profile propagation: #19849 5. Linux bubblewrap adapter: #19852 ## Validation 1. codex protocol permissions tests 2. formatting for codex protocol and codex linux sandbox 3. diff whitespace check --- codex-rs/linux-sandbox/src/bwrap.rs | 1 + codex-rs/protocol/src/permissions.rs | 367 ++++++++++++++++++++++----- codex-rs/protocol/src/protocol.rs | 128 ++-------- 3 files changed, 326 insertions(+), 170 deletions(-) diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index f2adf3d04..e191a924d 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -259,6 +259,7 @@ fn create_filesystem_args( writable_roots.push(WritableRoot { root: AbsolutePathBuf::from_absolute_path("/")?, read_only_subpaths: Vec::new(), + protected_metadata_names: Vec::new(), }); } let missing_auto_metadata_read_only_project_root_subpaths: HashSet = diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 2ce5194e0..c1f3d0724 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -19,6 +19,62 @@ use crate::protocol::NetworkAccess; use crate::protocol::SandboxPolicy; use crate::protocol::WritableRoot; +const PROTECTED_METADATA_GIT_PATH_NAME: &str = ".git"; +const PROTECTED_METADATA_AGENTS_PATH_NAME: &str = ".agents"; +const PROTECTED_METADATA_CODEX_PATH_NAME: &str = ".codex"; + +/// Top-level workspace metadata paths that stay protected under writable roots. +pub const PROTECTED_METADATA_PATH_NAMES: &[&str] = &[ + PROTECTED_METADATA_GIT_PATH_NAME, + PROTECTED_METADATA_AGENTS_PATH_NAME, + PROTECTED_METADATA_CODEX_PATH_NAME, +]; + +/// Returns true when a path basename is one of the protected workspace metadata names. +pub fn is_protected_metadata_name(name: &OsStr) -> bool { + PROTECTED_METADATA_PATH_NAMES + .iter() + .any(|metadata_name| name == OsStr::new(metadata_name)) +} + +pub fn is_protected_metadata_directory_name(name: &OsStr) -> bool { + name == OsStr::new(PROTECTED_METADATA_AGENTS_PATH_NAME) + || name == OsStr::new(PROTECTED_METADATA_CODEX_PATH_NAME) +} + +/// Returns the protected workspace metadata name when an agent write to `path` +/// should be blocked before execution. +pub fn forbidden_agent_metadata_write( + path: &Path, + cwd: &Path, + file_system_sandbox_policy: &FileSystemSandboxPolicy, +) -> Option<&'static str> { + if !matches!( + file_system_sandbox_policy.kind, + FileSystemSandboxKind::Restricted + ) { + return None; + } + + let target = resolve_candidate_path(path, cwd)?; + let (protected_metadata_path, metadata_name) = + metadata_child_of_writable_root(file_system_sandbox_policy, target.as_path(), cwd)?; + if has_explicit_write_entry_for_metadata_path( + file_system_sandbox_policy, + &protected_metadata_path, + target.as_path(), + cwd, + ) { + return None; + } + + if !file_system_sandbox_policy.can_write_path_with_cwd(target.as_path(), cwd) { + return Some(metadata_name); + } + + None +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS, )] @@ -570,7 +626,35 @@ impl FileSystemSandboxPolicy { } pub fn can_write_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool { - self.resolve_access_with_cwd(path, cwd).can_write() + if !self.resolve_access_with_cwd(path, cwd).can_write() { + return false; + } + if self.has_full_disk_write_access() { + return true; + } + !self.is_metadata_write_denied(path, cwd) + } + + fn is_metadata_write_denied(&self, path: &Path, cwd: &Path) -> bool { + if !matches!(self.kind, FileSystemSandboxKind::Restricted) { + return false; + } + + let Some(target) = resolve_candidate_path(path, cwd) else { + return true; + }; + let Some((protected_metadata_path, _)) = + metadata_child_of_writable_root(self, target.as_path(), cwd) + else { + return false; + }; + + !has_explicit_write_entry_for_metadata_path( + self, + &protected_metadata_path, + target.as_path(), + cwd, + ) } /// Replaces symbolic `:project_roots` entries with absolute paths resolved @@ -688,6 +772,10 @@ impl FileSystemSandboxPolicy { return true; }; + if protected_metadata_names_need_direct_runtime_enforcement(self, &legacy_policy, cwd) { + return true; + } + self.semantic_signature(cwd) != legacy_runtime_file_system_policy_for_cwd(&legacy_policy, cwd) .semantic_signature(cwd) @@ -749,6 +837,8 @@ impl FileSystemSandboxPolicy { .iter() .filter(|path| normalize_effective_absolute_path((*path).clone()) == root) .collect(); + let protected_metadata_names = + protected_metadata_names_for_writable_root(self, &root, &raw_writable_roots, cwd); let protect_missing_dot_codex = AbsolutePathBuf::from_absolute_path(cwd) .ok() .is_some_and(|cwd| normalize_effective_absolute_path(cwd) == root); @@ -816,6 +906,7 @@ impl FileSystemSandboxPolicy { }), ); WritableRoot { + protected_metadata_names, root, // Preserve literal in-root protected paths like `.git` and // `.codex` so downstream sandboxes can still detect and mask @@ -1084,7 +1175,7 @@ fn resolve_candidate_path(path: &Path, cwd: &Path) -> Option { if path.is_absolute() { AbsolutePathBuf::from_absolute_path(path).ok() } else { - Some(AbsolutePathBuf::resolve_path_against_base(path, cwd)) + Some(AbsolutePathBuf::from_absolute_path(cwd).ok()?.join(path)) } } @@ -1277,6 +1368,8 @@ fn sorted_writable_roots(mut roots: Vec) -> Vec { for root in &mut roots { root.read_only_subpaths = sorted_absolute_paths(std::mem::take(&mut root.read_only_subpaths)); + root.protected_metadata_names.sort(); + root.protected_metadata_names.dedup(); } roots.sort_by(|left, right| left.root.as_path().cmp(right.root.as_path())); roots @@ -1303,18 +1396,19 @@ fn normalize_effective_absolute_path(path: AbsolutePathBuf) -> AbsolutePathBuf { path } -fn default_read_only_subpaths_for_writable_root( +pub(crate) fn default_read_only_subpaths_for_writable_root( writable_root: &AbsolutePathBuf, protect_missing_dot_codex: bool, ) -> Vec { let mut subpaths: Vec = Vec::new(); - let top_level_git = writable_root.join(".git"); + let top_level_git = writable_root.join(PROTECTED_METADATA_GIT_PATH_NAME); // This applies to typical repos (directory .git), worktrees/submodules // (file .git with gitdir pointer), and bare repos when the gitdir is the // writable root itself. let top_level_git_is_file = top_level_git.as_path().is_file(); let top_level_git_is_dir = top_level_git.as_path().is_dir(); - if top_level_git_is_dir || top_level_git_is_file { + let should_protect_top_level = top_level_git_is_dir || top_level_git_is_file; + if should_protect_top_level { if top_level_git_is_file && is_git_pointer_file(&top_level_git) && let Some(gitdir) = resolve_gitdir_from_file(&top_level_git) @@ -1324,7 +1418,7 @@ fn default_read_only_subpaths_for_writable_root( subpaths.push(top_level_git); } - let top_level_agents = writable_root.join(".agents"); + let top_level_agents = writable_root.join(PROTECTED_METADATA_AGENTS_PATH_NAME); if top_level_agents.as_path().is_dir() { subpaths.push(top_level_agents); } @@ -1333,7 +1427,7 @@ fn default_read_only_subpaths_for_writable_root( // default. For the workspace root itself, protect it even before the // directory exists so first-time creation still goes through the // protected-path approval flow. - let top_level_codex = writable_root.join(".codex"); + let top_level_codex = writable_root.join(PROTECTED_METADATA_CODEX_PATH_NAME); if protect_missing_dot_codex || top_level_codex.as_path().is_dir() { subpaths.push(top_level_codex); } @@ -1465,8 +1559,105 @@ fn has_explicit_resolved_path_entry( entries.iter().any(|entry| &entry.path == path) } +fn metadata_path_name(name: &OsStr) -> Option<&'static str> { + PROTECTED_METADATA_PATH_NAMES + .iter() + .copied() + .find(|metadata_name| name == OsStr::new(metadata_name)) +} + +fn metadata_child_of_writable_root( + policy: &FileSystemSandboxPolicy, + target: &Path, + cwd: &Path, +) -> Option<(AbsolutePathBuf, &'static str)> { + policy + .resolved_entries_with_cwd(cwd) + .iter() + .filter(|entry| entry.access.can_write()) + .filter_map(|entry| { + let relative_path = target.strip_prefix(entry.path.as_path()).ok()?; + let first_component = relative_path.components().next()?; + let metadata_name = metadata_path_name(first_component.as_os_str())?; + Some((entry.path.join(metadata_name), metadata_name)) + }) + .next() +} + +fn protected_metadata_names_for_writable_root( + policy: &FileSystemSandboxPolicy, + root: &AbsolutePathBuf, + raw_writable_roots: &[&AbsolutePathBuf], + cwd: &Path, +) -> Vec { + let mut protected_names = Vec::new(); + for metadata_name in PROTECTED_METADATA_PATH_NAMES { + let mut metadata_paths = vec![root.join(*metadata_name)]; + metadata_paths.extend( + raw_writable_roots + .iter() + .map(|raw_root| raw_root.join(*metadata_name)), + ); + + if metadata_paths + .iter() + .all(|metadata_path| !policy.can_write_path_with_cwd(metadata_path.as_path(), cwd)) + { + protected_names.push((*metadata_name).to_string()); + } + } + protected_names +} + +fn protected_metadata_names_need_direct_runtime_enforcement( + policy: &FileSystemSandboxPolicy, + legacy_policy: &SandboxPolicy, + cwd: &Path, +) -> bool { + let legacy_roots = legacy_policy.get_writable_roots_with_cwd(cwd); + policy + .get_writable_roots_with_cwd(cwd) + .into_iter() + .any(|writable_root| { + let Some(legacy_root) = legacy_roots + .iter() + .find(|candidate| candidate.root == writable_root.root) + else { + return !writable_root.protected_metadata_names.is_empty(); + }; + + writable_root + .protected_metadata_names + .iter() + .any(|metadata_name| { + let metadata_path = writable_root.root.join(metadata_name); + !legacy_root + .read_only_subpaths + .iter() + .any(|subpath| subpath == &metadata_path) + }) + }) +} + +fn has_explicit_write_entry_for_metadata_path( + policy: &FileSystemSandboxPolicy, + protected_metadata_path: &AbsolutePathBuf, + target: &Path, + cwd: &Path, +) -> bool { + policy.resolved_entries_with_cwd(cwd).iter().any(|entry| { + entry.access.can_write() + && target.starts_with(entry.path.as_path()) + && entry + .path + .as_path() + .starts_with(protected_metadata_path.as_path()) + }) +} + fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool { - path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git")) + path.as_path().is_file() + && path.as_path().file_name() == Some(OsStr::new(PROTECTED_METADATA_GIT_PATH_NAME)) } fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option { @@ -1483,7 +1674,14 @@ fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option parts, + Some((prefix, gitdir_raw)) if prefix.trim() == "gitdir" => (prefix, gitdir_raw), + Some(_) => { + error!( + "Expected {path} to contain a gitdir pointer, but it did not match `gitdir: `.", + path = dot_git.as_path().display() + ); + return None; + } None => { error!( "Expected {path} to contain a gitdir pointer, but it did not match `gitdir: `.", @@ -1696,6 +1894,12 @@ mod tests { .iter() .find(|root| root.root == expected_root) .expect("workspace writable root"); + assert!( + !workspace_root + .protected_metadata_names + .contains(&".codex".to_string()), + "explicit .codex rule should remove the metadata-name protection" + ); assert!( !workspace_root .read_only_subpaths @@ -1711,32 +1915,46 @@ mod tests { } #[test] - fn legacy_workspace_write_projection_blocks_missing_dot_codex_writes() { + fn filesystem_policy_blocks_protected_metadata_path_writes_by_default() { let cwd = TempDir::new().expect("tempdir"); + let dot_git_config = cwd.path().join(".git").join("config"); + let dot_agents_config = cwd.path().join(".agents").join("config"); let dot_codex_config = cwd.path().join(".codex").join("config.toml"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - + let root = AbsolutePathBuf::from_absolute_path(cwd.path()).expect("absolute cwd"); let file_system_policy = - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, cwd.path()); + FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { path: root }, + access: FileSystemAccessMode::Write, + }]); + assert!(!file_system_policy.can_write_path_with_cwd(&dot_git_config, cwd.path())); + assert!(!file_system_policy.can_write_path_with_cwd(&dot_agents_config, cwd.path())); assert!(!file_system_policy.can_write_path_with_cwd(&dot_codex_config, cwd.path())); + + let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd.path()); + assert_eq!(writable_roots.len(), 1); + assert_eq!( + writable_roots[0].protected_metadata_names, + vec![ + ".git".to_string(), + ".agents".to_string(), + ".codex".to_string(), + ] + ); + assert!(!writable_roots[0].is_path_writable(&dot_git_config)); + assert!(!writable_roots[0].is_path_writable(&dot_agents_config)); + assert!(!writable_roots[0].is_path_writable(&dot_codex_config)); } #[test] fn legacy_workspace_write_projection_accepts_relative_cwd() { let relative_cwd = Path::new("workspace"); - let expected_dot_codex = AbsolutePathBuf::from_absolute_path( + let expected_root = AbsolutePathBuf::from_absolute_path( std::env::current_dir() .expect("current dir") - .join(relative_cwd) - .join(".codex"), + .join(relative_cwd), ) - .expect("absolute dot codex"); + .expect("absolute root"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, @@ -1747,51 +1965,62 @@ mod tests { let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, relative_cwd); + let mut expected_entries = vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Write, + }, + ]; + expected_entries.extend(PROTECTED_METADATA_PATH_NAMES.iter().map(|name| { + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some((*name).into())), + }, + access: FileSystemAccessMode::Read, + } + })); + expected_entries.extend( + default_read_only_subpaths_for_writable_root( + &expected_root, + /*protect_missing_dot_codex*/ true, + ) + .into_iter() + .map(|path| FileSystemSandboxEntry { + path: FileSystemPath::Path { path }, + access: FileSystemAccessMode::Read, + }), + ); + assert_eq!( file_system_policy, - FileSystemSandboxPolicy::restricted(vec![ - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::project_roots(/*subpath*/ None), - }, - access: FileSystemAccessMode::Write, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::project_roots(Some(".git".into())), - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::project_roots(Some(".agents".into())), - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::project_roots(Some(".codex".into())), - }, - access: FileSystemAccessMode::Read, - }, - FileSystemSandboxEntry { - path: FileSystemPath::Path { - path: expected_dot_codex, - }, - access: FileSystemAccessMode::Read, - }, - ]) + FileSystemSandboxPolicy::restricted(expected_entries) + ); + assert_eq!( + forbidden_agent_metadata_write( + Path::new(".git/config"), + relative_cwd, + &file_system_policy, + ), + Some(".git") ); assert!( !file_system_policy .can_write_path_with_cwd(Path::new(".codex/config.toml"), relative_cwd,) ); + assert!( + !file_system_policy.can_write_path_with_cwd( + Path::new(".agents/skills/example/SKILL.md"), + relative_cwd, + ) + ); } #[cfg(unix)] @@ -2233,8 +2462,9 @@ mod tests { cwd.path(), ); assert!( - !legacy_workspace_write - .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path(),) + legacy_workspace_write + .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path(),), + "metadata-name protections must stay in the direct enforcement path even when legacy concrete read-only paths match" ); } @@ -2256,9 +2486,12 @@ mod tests { legacy_order.is_semantically_equivalent_to(&reordered, cwd.path()), "entry order should not affect filesystem semantics" ); - assert!( - !reordered - .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path()) + assert_eq!( + legacy_order + .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path()), + reordered + .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path()), + "entry order should not affect direct-enforcement classification" ); } @@ -2283,9 +2516,9 @@ mod tests { let legacy_runtime_projection = legacy_runtime_file_system_policy_for_cwd(&legacy_policy, cwd.path()); assert!( - !legacy_runtime_projection + legacy_runtime_projection .needs_direct_runtime_enforcement(NetworkSandboxPolicy::Restricted, cwd.path()), - "true legacy runtime expansion should still classify as legacy-compatible" + "metadata-name protections are outside the legacy SandboxPolicy writable-root contract" ); } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 287662702..34e5a4794 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -4,8 +4,6 @@ //! between user and agent. use std::collections::HashMap; -use std::collections::HashSet; -use std::ffi::OsStr; use std::fmt; use std::ops::Mul; use std::path::Path; @@ -84,6 +82,7 @@ pub use crate::permissions::FileSystemSandboxKind; pub use crate::permissions::FileSystemSandboxPolicy; pub use crate::permissions::FileSystemSpecialPath; pub use crate::permissions::NetworkSandboxPolicy; +use crate::permissions::default_read_only_subpaths_for_writable_root; pub use crate::request_permissions::RequestPermissionsArgs; pub use crate::request_user_input::RequestUserInputEvent; @@ -1087,6 +1086,11 @@ pub struct WritableRoot { /// By construction, these subpaths are all under `root`. pub read_only_subpaths: Vec, + + /// Workspace metadata path names that must not be created or replaced under + /// `root` unless the policy grants an explicit write rule for that metadata + /// path. + pub protected_metadata_names: Vec, } impl WritableRoot { @@ -1103,8 +1107,26 @@ impl WritableRoot { } } + if self.path_contains_protected_metadata_name(path) { + return false; + } + true } + + fn path_contains_protected_metadata_name(&self, path: &Path) -> bool { + let Ok(relative_path) = path.strip_prefix(&self.root) else { + return false; + }; + + let Some(first_component) = relative_path.components().next() else { + return false; + }; + + self.protected_metadata_names + .iter() + .any(|name| first_component.as_os_str() == std::ffi::OsStr::new(name)) + } } impl FromStr for SandboxPolicy { @@ -1257,6 +1279,7 @@ impl SandboxPolicy { &writable_root, protect_missing_dot_codex, ), + protected_metadata_names: Vec::new(), root: writable_root, } }) @@ -1266,107 +1289,6 @@ impl SandboxPolicy { } } -fn default_read_only_subpaths_for_writable_root( - writable_root: &AbsolutePathBuf, - protect_missing_dot_codex: bool, -) -> Vec { - let mut subpaths: Vec = Vec::new(); - let top_level_git = writable_root.join(".git"); - // This applies to typical repos (directory .git), worktrees/submodules - // (file .git with gitdir pointer), and bare repos when the gitdir is the - // writable root itself. - let top_level_git_is_file = top_level_git.as_path().is_file(); - let top_level_git_is_dir = top_level_git.as_path().is_dir(); - if top_level_git_is_dir || top_level_git_is_file { - if top_level_git_is_file - && is_git_pointer_file(&top_level_git) - && let Some(gitdir) = resolve_gitdir_from_file(&top_level_git) - { - subpaths.push(gitdir); - } - subpaths.push(top_level_git); - } - - let top_level_agents = writable_root.join(".agents"); - if top_level_agents.as_path().is_dir() { - subpaths.push(top_level_agents); - } - - // Keep top-level project metadata under .codex read-only to the agent by - // default. For the workspace root itself, protect it even before the - // directory exists so first-time creation still goes through the - // protected-path approval flow. - let top_level_codex = writable_root.join(".codex"); - if protect_missing_dot_codex || top_level_codex.as_path().is_dir() { - subpaths.push(top_level_codex); - } - - let mut deduped = Vec::with_capacity(subpaths.len()); - let mut seen = HashSet::new(); - for path in subpaths { - if seen.insert(path.to_path_buf()) { - deduped.push(path); - } - } - deduped -} - -fn is_git_pointer_file(path: &AbsolutePathBuf) -> bool { - path.as_path().is_file() && path.as_path().file_name() == Some(OsStr::new(".git")) -} - -fn resolve_gitdir_from_file(dot_git: &AbsolutePathBuf) -> Option { - let contents = match std::fs::read_to_string(dot_git.as_path()) { - Ok(contents) => contents, - Err(err) => { - error!( - "Failed to read {path} for gitdir pointer: {err}", - path = dot_git.as_path().display() - ); - return None; - } - }; - - let trimmed = contents.trim(); - let (_, gitdir_raw) = match trimmed.split_once(':') { - Some(parts) => parts, - None => { - error!( - "Expected {path} to contain a gitdir pointer, but it did not match `gitdir: `.", - path = dot_git.as_path().display() - ); - return None; - } - }; - let gitdir_raw = gitdir_raw.trim(); - if gitdir_raw.is_empty() { - error!( - "Expected {path} to contain a gitdir pointer, but it was empty.", - path = dot_git.as_path().display() - ); - return None; - } - let base = match dot_git.as_path().parent() { - Some(base) => base, - None => { - error!( - "Unable to resolve parent directory for {path}.", - path = dot_git.as_path().display() - ); - return None; - } - }; - let gitdir_path = AbsolutePathBuf::resolve_path_against_base(gitdir_raw, base); - if !gitdir_path.as_path().exists() { - error!( - "Resolved gitdir path {path} does not exist.", - path = gitdir_path.as_path().display() - ); - return None; - } - Some(gitdir_path) -} - /// Event Queue Entry - events from agent #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Event {