mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user