mirror of
https://github.com/openai/codex.git
synced 2026-05-28 15:00:16 +00:00
memories-mcp: hide dot paths from list, read, and search (#21201)
## Why The local memories root can contain implementation details such as `.git` plus incidental OS metadata like `.DS_Store`. Those entries are not authored memory content, so the memories MCP should keep them invisible instead of exposing them through normal discovery or direct lookup. Only for local implementation ofc ## What changed - Return `NotFound` for scoped `list`, `read`, and `search` requests that include a hidden path component. - Skip hidden files and directories while listing a directory or recursively searching the memories tree. - Add regression coverage for hidden files, hidden directories, and hidden scoped requests across `list`, `read`, and `search`. ## Testing - Added focused regression tests in `memories/mcp/src/local_tests.rs` covering hidden-path behavior across the affected APIs.
This commit is contained in:
@@ -58,6 +58,11 @@ impl LocalMemoriesBackend {
|
||||
"must stay within the memories root",
|
||||
));
|
||||
}
|
||||
if relative.components().any(is_hidden_component) {
|
||||
return Err(MemoriesBackendError::NotFound {
|
||||
path: relative_path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let components = relative.components().collect::<Vec<_>>();
|
||||
let mut scoped_path = self.root.clone();
|
||||
@@ -122,6 +127,9 @@ impl MemoriesBackend for LocalMemoriesBackend {
|
||||
} else if metadata.is_dir() {
|
||||
let mut entries = Vec::new();
|
||||
for path in read_sorted_dir_paths(&start).await? {
|
||||
if is_hidden_path(&path) {
|
||||
continue;
|
||||
}
|
||||
let Some(metadata) = Self::metadata_or_none(&path).await? else {
|
||||
continue;
|
||||
};
|
||||
@@ -288,6 +296,9 @@ async fn search_entries(
|
||||
let mut pending = vec![current.to_path_buf()];
|
||||
while let Some(dir_path) = pending.pop() {
|
||||
for path in read_sorted_dir_paths(&dir_path).await? {
|
||||
if is_hidden_path(&path) {
|
||||
continue;
|
||||
}
|
||||
let Some(metadata) = LocalMemoriesBackend::metadata_or_none(&path).await? else {
|
||||
continue;
|
||||
};
|
||||
@@ -403,6 +414,18 @@ fn reject_symlink(path: &str, metadata: &std::fs::Metadata) -> Result<(), Memori
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_hidden_component(component: Component<'_>) -> bool {
|
||||
matches!(
|
||||
component,
|
||||
Component::Normal(name) if name.to_string_lossy().starts_with('.')
|
||||
)
|
||||
}
|
||||
|
||||
fn is_hidden_path(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.is_some_and(|name| name.to_string_lossy().starts_with('.'))
|
||||
}
|
||||
|
||||
fn display_relative_path(root: &Path, path: &Path) -> String {
|
||||
path.strip_prefix(root)
|
||||
.unwrap_or(path)
|
||||
|
||||
@@ -26,9 +26,15 @@ async fn list_returns_shallow_memory_paths() {
|
||||
tokio::fs::create_dir_all(tempdir.path().join("skills/example"))
|
||||
.await
|
||||
.expect("create skills dir");
|
||||
tokio::fs::create_dir_all(tempdir.path().join(".git"))
|
||||
.await
|
||||
.expect("create hidden dir");
|
||||
tokio::fs::write(tempdir.path().join("MEMORY.md"), "summary")
|
||||
.await
|
||||
.expect("write memory file");
|
||||
tokio::fs::write(tempdir.path().join(".DS_Store"), "metadata")
|
||||
.await
|
||||
.expect("write hidden file");
|
||||
tokio::fs::write(tempdir.path().join("skills/example/SKILL.md"), "skill")
|
||||
.await
|
||||
.expect("write skill file");
|
||||
@@ -193,6 +199,25 @@ async fn list_scoped_directory_is_shallow() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_rejects_hidden_scoped_paths() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
tokio::fs::create_dir_all(tempdir.path().join(".git"))
|
||||
.await
|
||||
.expect("create hidden dir");
|
||||
|
||||
let err = backend(&tempdir)
|
||||
.list(ListMemoriesRequest {
|
||||
path: Some(".git".to_string()),
|
||||
cursor: None,
|
||||
max_results: DEFAULT_LIST_MAX_RESULTS,
|
||||
})
|
||||
.await
|
||||
.expect_err("hidden scoped paths should stay invisible");
|
||||
|
||||
assert!(matches!(err, MemoriesBackendError::NotFound { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_rejects_invalid_cursor() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
@@ -315,6 +340,29 @@ async fn read_supports_line_offset() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_rejects_hidden_paths() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
tokio::fs::create_dir_all(tempdir.path().join(".git"))
|
||||
.await
|
||||
.expect("create hidden dir");
|
||||
tokio::fs::write(tempdir.path().join(".git/HEAD"), "ref: refs/heads/main\n")
|
||||
.await
|
||||
.expect("write hidden file");
|
||||
|
||||
let err = backend(&tempdir)
|
||||
.read(ReadMemoryRequest {
|
||||
path: ".git/HEAD".to_string(),
|
||||
line_offset: 1,
|
||||
max_lines: None,
|
||||
max_tokens: DEFAULT_READ_MAX_TOKENS,
|
||||
})
|
||||
.await
|
||||
.expect_err("hidden paths should stay invisible");
|
||||
|
||||
assert!(matches!(err, MemoriesBackendError::NotFound { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_supports_max_lines() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
@@ -559,6 +607,56 @@ async fn search_preserves_global_lexicographic_path_order() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_skips_hidden_paths() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
tokio::fs::create_dir_all(tempdir.path().join(".git"))
|
||||
.await
|
||||
.expect("create hidden dir");
|
||||
tokio::fs::write(tempdir.path().join("MEMORY.md"), "needle visible\n")
|
||||
.await
|
||||
.expect("write visible file");
|
||||
tokio::fs::write(tempdir.path().join(".git/HEAD"), "needle hidden\n")
|
||||
.await
|
||||
.expect("write hidden file");
|
||||
tokio::fs::write(tempdir.path().join(".hidden"), "needle hidden\n")
|
||||
.await
|
||||
.expect("write hidden file");
|
||||
|
||||
let response = backend(&tempdir)
|
||||
.search(search_request(&["needle"]))
|
||||
.await
|
||||
.expect("search memories");
|
||||
|
||||
assert_eq!(
|
||||
response.matches,
|
||||
vec![MemorySearchMatch {
|
||||
path: "MEMORY.md".to_string(),
|
||||
match_line_number: 1,
|
||||
content_start_line_number: 1,
|
||||
content: "needle visible".to_string(),
|
||||
matched_queries: vec!["needle".to_string()],
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_rejects_hidden_scoped_paths() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
tokio::fs::create_dir_all(tempdir.path().join(".git"))
|
||||
.await
|
||||
.expect("create hidden dir");
|
||||
|
||||
let mut request = search_request(&["needle"]);
|
||||
request.path = Some(".git".to_string());
|
||||
let err = backend(&tempdir)
|
||||
.search(request)
|
||||
.await
|
||||
.expect_err("hidden scoped paths should stay invisible");
|
||||
|
||||
assert!(matches!(err, MemoriesBackendError::NotFound { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_supports_context_lines() {
|
||||
let tempdir = TempDir::new().expect("tempdir");
|
||||
|
||||
Reference in New Issue
Block a user