fix: update tests and fix bugs for project-prefixed permalinks

- Fix update_entity overwriting project-prefixed permalink with
  non-prefixed one during metadata merge
- Fix memory:// URL path traversal validation bypass in read_note
  and read_content tools
- Fix pyright type error in claude_projects_importer
- Update test assertions across 12 test files to use project-prefixed
  permalinks (test-project/path instead of path)
- Fix monkeypatch in search test to use resolve_project_and_path
  instead of removed get_active_project

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-02-13 09:18:31 -06:00
parent c6511aa745
commit b367ccbdb8
15 changed files with 91 additions and 60 deletions
@@ -71,7 +71,8 @@ class ClaudeProjectsImporter(Importer[ProjectImportResult]):
)
permalink, file_path = self.build_import_paths(prompt_path)
prompt_entity = self._format_prompt_markdown(project, permalink)
await self.write_entity(prompt_entity, file_path)
if prompt_entity:
await self.write_entity(prompt_entity, file_path)
prompts_imported += 1
# Import project documents
+7 -1
View File
@@ -18,6 +18,7 @@ from mcp.server.fastmcp.exceptions import ToolError
from basic_memory.mcp.project_context import get_project_client, resolve_project_and_path
from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.utils import call_get, resolve_entity_id
from basic_memory.schemas.memory import memory_url_path
from basic_memory.utils import validate_project_path
@@ -205,8 +206,13 @@ async def read_content(
_, url, _ = await resolve_project_and_path(client, path, project, context)
# Validate path to prevent path traversal attacks
# For memory:// URLs, validate the extracted path (not the raw URL which
# has a scheme prefix that confuses path validation)
raw_path = memory_url_path(path) if path.startswith("memory://") else path
project_path = active_project.home
if not validate_project_path(url, project_path):
if not validate_project_path(raw_path, project_path) or not validate_project_path(
url, project_path
):
logger.warning(
"Attempted path traversal attack blocked",
path=path,
+5 -2
View File
@@ -10,6 +10,7 @@ from basic_memory.mcp.project_context import get_project_client, resolve_project
from basic_memory.mcp.server import mcp
from basic_memory.mcp.formatting import format_note_preview_ascii
from basic_memory.mcp.tools.search import search_notes
from basic_memory.schemas.memory import memory_url_path
from basic_memory.utils import validate_project_path
@@ -87,11 +88,13 @@ async def read_note(
)
# Validate identifier to prevent path traversal attacks
# We need to check both the raw identifier and the processed path
# For memory:// URLs, validate the extracted path (not the raw URL which
# has a scheme prefix that confuses path validation)
raw_path = memory_url_path(identifier) if identifier.startswith("memory://") else identifier
processed_path = entity_path
project_path = active_project.home
if not validate_project_path(identifier, project_path) or not validate_project_path(
if not validate_project_path(raw_path, project_path) or not validate_project_path(
processed_path, project_path
):
logger.warning(
+7 -4
View File
@@ -42,7 +42,7 @@ from basic_memory.services.exceptions import (
)
from basic_memory.services.link_resolver import LinkResolver
from basic_memory.services.search_service import SearchService
from basic_memory.utils import build_canonical_permalink, generate_permalink
from basic_memory.utils import build_canonical_permalink
class EntityService(BaseService[EntityModel]):
@@ -346,9 +346,12 @@ class EntityService(BaseService[EntityModel]):
# Merge new metadata with existing metadata
existing_markdown.frontmatter.metadata.update(post.metadata)
# Ensure the permalink in the metadata is the resolved one
if new_permalink != entity.permalink:
existing_markdown.frontmatter.metadata["permalink"] = new_permalink
# Always ensure the permalink in the metadata is the canonical one from the database.
# The schema_to_markdown call above uses EntitySchema.permalink which computes a
# non-prefixed permalink (e.g., "test/note"). The metadata merge on the previous line
# would overwrite the project-prefixed permalink (e.g., "project/test/note") stored
# in the existing file. Setting it unconditionally preserves the correct value.
existing_markdown.frontmatter.metadata["permalink"] = new_permalink
# Create a new post with merged metadata
merged_post = frontmatter.Post(post.content, **existing_markdown.frontmatter.metadata)