Skip to content

Commit 4bff540

Browse files
jope-bmclaude
andcommitted
chore: apply lint and formatting fixes
Apply automatic code formatting and linting fixes from ruff formatter. These changes ensure consistent code style across the codebase without functional modifications. Changes: - Fix whitespace and line ending consistency - Apply consistent string quote formatting - Remove trailing whitespace - Format multi-line expressions for better readability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: jope-bm <jope-bm@users.noreply.github.com> Signed-off-by: Joe P <joe@basicmemory.com>
1 parent 152c8de commit 4bff540

7 files changed

Lines changed: 120 additions & 103 deletions

File tree

src/basic_memory/mcp/tools/read_note.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async def read_note(
6868

6969
# Get the file via REST API - first try direct permalink lookup
7070
entity_path = memory_url_path(identifier)
71-
71+
7272
# Validate path to prevent path traversal attacks
7373
project_path = active_project.home
7474
if not validate_project_path(entity_path, project_path):

src/basic_memory/utils.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,23 +220,23 @@ def validate_project_path(path: str, project_path: Path) -> bool:
220220
# Allow empty strings as they resolve to the project root
221221
if not path:
222222
return True
223-
223+
224224
# Check for obvious path traversal patterns first
225225
if ".." in path or "~" in path:
226226
return False
227-
227+
228228
# Check for Windows-style path traversal (even on Unix systems)
229229
if "\\.." in path or path.startswith("\\"):
230230
return False
231-
231+
232232
# Block absolute paths (Unix-style starting with / or Windows-style with drive letters)
233233
if path.startswith("/") or (len(path) >= 2 and path[1] == ":"):
234234
return False
235-
235+
236236
# Block paths with control characters (but allow whitespace that will be stripped)
237-
if path.strip() and any(ord(c) < 32 and c not in [' ', '\t'] for c in path):
237+
if path.strip() and any(ord(c) < 32 and c not in [" ", "\t"] for c in path):
238238
return False
239-
239+
240240
try:
241241
resolved = (project_path / path).resolve()
242242
return resolved.is_relative_to(project_path.resolve())

tests/mcp/test_tool_move_note.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ async def test_move_note_allows_safe_paths(self, client):
650650
assert isinstance(result, str)
651651
# Should NOT contain security error message
652652
assert "Security Validation Error" not in result
653-
653+
654654
# If it fails, it should be for other reasons like "already exists" or API errors
655655
if "Move Failed" in result:
656656
assert "paths must stay within project boundaries" not in result
@@ -672,7 +672,7 @@ async def test_move_note_security_logging(self, client, caplog):
672672
)
673673

674674
assert "# Move Failed - Security Validation Error" in result
675-
675+
676676
# Check that security violation was logged
677677
# Note: This test may need adjustment based on the actual logging setup
678678
# The security validation should generate a warning log entry
@@ -710,7 +710,7 @@ async def test_move_note_current_directory_references_security(self, client):
710710
# Test current directory references (should be safe)
711711
safe_paths = [
712712
"./notes/file.md",
713-
"folder/./file.md",
713+
"folder/./file.md",
714714
"./folder/subfolder/file.md",
715715
]
716716

tests/mcp/test_tool_read_content.py

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -139,18 +139,19 @@ async def test_read_content_allows_safe_paths_with_mocked_api(self, client):
139139
# Mock the API call to simulate a successful response
140140
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
141141
mock_response = MagicMock()
142-
mock_response.headers = {
143-
"content-type": "text/markdown",
144-
"content-length": "100"
145-
}
142+
mock_response.headers = {"content-type": "text/markdown", "content-length": "100"}
146143
mock_response.text = f"# Content for {safe_path}\nThis is test content."
147144
mock_call_get.return_value = mock_response
148-
145+
149146
result = await read_content.fn(path=safe_path)
150147

151148
# Should succeed (not a security error)
152149
assert isinstance(result, dict)
153-
assert result["type"] != "error" or "paths must stay within project boundaries" not in result.get("error", "")
150+
assert result[
151+
"type"
152+
] != "error" or "paths must stay within project boundaries" not in result.get(
153+
"error", ""
154+
)
154155

155156
@pytest.mark.asyncio
156157
async def test_read_content_memory_url_processing(self, client):
@@ -178,7 +179,7 @@ async def test_read_content_security_logging(self, client, caplog):
178179

179180
assert result["type"] == "error"
180181
assert "paths must stay within project boundaries" in result["error"]
181-
182+
182183
# Check that security violation was logged
183184
# Note: This test may need adjustment based on the actual logging setup
184185
# The security validation should generate a warning log entry
@@ -189,45 +190,47 @@ async def test_read_content_empty_path_security(self, client):
189190
# Mock the API call since empty path should be allowed (resolves to project root)
190191
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
191192
mock_response = MagicMock()
192-
mock_response.headers = {
193-
"content-type": "text/markdown",
194-
"content-length": "50"
195-
}
193+
mock_response.headers = {"content-type": "text/markdown", "content-length": "50"}
196194
mock_response.text = "# Root content"
197195
mock_call_get.return_value = mock_response
198-
196+
199197
result = await read_content.fn(path="")
200198

201199
assert isinstance(result, dict)
202200
# Empty path should not trigger security error (it's handled as project root)
203-
assert result["type"] != "error" or "paths must stay within project boundaries" not in result.get("error", "")
201+
assert result[
202+
"type"
203+
] != "error" or "paths must stay within project boundaries" not in result.get(
204+
"error", ""
205+
)
204206

205207
@pytest.mark.asyncio
206208
async def test_read_content_current_directory_references_security(self, client):
207209
"""Test that current directory references are handled securely."""
208210
# Test current directory references (should be safe)
209211
safe_paths = [
210212
"./notes/file.md",
211-
"folder/./file.md",
213+
"folder/./file.md",
212214
"./folder/subfolder/file.md",
213215
]
214216

215217
for safe_path in safe_paths:
216218
# Mock the API call for these safe paths
217219
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
218220
mock_response = MagicMock()
219-
mock_response.headers = {
220-
"content-type": "text/markdown",
221-
"content-length": "100"
222-
}
221+
mock_response.headers = {"content-type": "text/markdown", "content-length": "100"}
223222
mock_response.text = f"# Content for {safe_path}"
224223
mock_call_get.return_value = mock_response
225-
224+
226225
result = await read_content.fn(path=safe_path)
227226

228227
assert isinstance(result, dict)
229228
# Should NOT contain security error message
230-
assert result["type"] != "error" or "paths must stay within project boundaries" not in result.get("error", "")
229+
assert result[
230+
"type"
231+
] != "error" or "paths must stay within project boundaries" not in result.get(
232+
"error", ""
233+
)
231234

232235

233236
class TestReadContentFunctionality:
@@ -246,13 +249,10 @@ async def test_read_content_text_file_success(self, client):
246249
# Mock the API call to simulate reading the file
247250
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
248251
mock_response = MagicMock()
249-
mock_response.headers = {
250-
"content-type": "text/markdown",
251-
"content-length": "100"
252-
}
252+
mock_response.headers = {"content-type": "text/markdown", "content-length": "100"}
253253
mock_response.text = "# Test Document\nThis is test content for reading."
254254
mock_call_get.return_value = mock_response
255-
255+
256256
result = await read_content.fn(path="docs/test-document.md")
257257

258258
assert isinstance(result, dict)
@@ -267,16 +267,16 @@ async def test_read_content_image_file_handling(self, client):
267267
# Mock the API call to simulate reading an image
268268
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
269269
# Create a simple fake image data
270-
fake_image_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xdb\x00\x00\x00\x00IEND\xaeB`\x82'
271-
270+
fake_image_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xdb\x00\x00\x00\x00IEND\xaeB`\x82"
271+
272272
mock_response = MagicMock()
273273
mock_response.headers = {
274274
"content-type": "image/png",
275-
"content-length": str(len(fake_image_data))
275+
"content-length": str(len(fake_image_data)),
276276
}
277277
mock_response.content = fake_image_data
278278
mock_call_get.return_value = mock_response
279-
279+
280280
# Mock PIL Image processing
281281
with patch("basic_memory.mcp.tools.read_content.PILImage") as mock_pil:
282282
mock_img = MagicMock()
@@ -285,10 +285,10 @@ async def test_read_content_image_file_handling(self, client):
285285
mock_img.mode = "RGB"
286286
mock_img.getbands.return_value = ["R", "G", "B"]
287287
mock_pil.open.return_value = mock_img
288-
288+
289289
with patch("basic_memory.mcp.tools.read_content.optimize_image") as mock_optimize:
290290
mock_optimize.return_value = b"optimized_image_data"
291-
291+
292292
result = await read_content.fn(path="assets/safe-image.png")
293293

294294
assert isinstance(result, dict)
@@ -302,24 +302,22 @@ async def test_read_content_with_project_parameter(self, client):
302302
"""Test reading content with explicit project parameter."""
303303
# Mock the API call and project configuration
304304
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
305-
with patch("basic_memory.mcp.tools.read_content.get_active_project") as mock_get_project:
305+
with patch(
306+
"basic_memory.mcp.tools.read_content.get_active_project"
307+
) as mock_get_project:
306308
# Mock project configuration
307309
mock_project = MagicMock()
308310
mock_project.project_url = "http://test"
309311
mock_project.home = Path("/test/project")
310312
mock_get_project.return_value = mock_project
311-
313+
312314
mock_response = MagicMock()
313-
mock_response.headers = {
314-
"content-type": "text/plain",
315-
"content-length": "50"
316-
}
315+
mock_response.headers = {"content-type": "text/plain", "content-length": "50"}
317316
mock_response.text = "Project-specific content"
318317
mock_call_get.return_value = mock_response
319-
318+
320319
result = await read_content.fn(
321-
path="notes/project-file.txt",
322-
project="specific-project"
320+
path="notes/project-file.txt", project="specific-project"
323321
)
324322

325323
assert isinstance(result, dict)
@@ -332,7 +330,7 @@ async def test_read_content_nonexistent_file_handling(self, client):
332330
# Mock API call to return 404
333331
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
334332
mock_call_get.side_effect = Exception("File not found")
335-
333+
336334
# This should pass security validation but fail on API call
337335
try:
338336
result = await read_content.fn(path="docs/nonexistent-file.md")
@@ -348,15 +346,15 @@ async def test_read_content_binary_file_handling(self, client):
348346
# Mock the API call to simulate reading a binary file
349347
with patch("basic_memory.mcp.tools.read_content.call_get") as mock_call_get:
350348
binary_data = b"Binary file content with special bytes: \x00\x01\x02\x03"
351-
349+
352350
mock_response = MagicMock()
353351
mock_response.headers = {
354352
"content-type": "application/octet-stream",
355-
"content-length": str(len(binary_data))
353+
"content-length": str(len(binary_data)),
356354
}
357355
mock_response.content = binary_data
358356
mock_call_get.return_value = mock_response
359-
357+
360358
result = await read_content.fn(path="files/safe-binary.bin")
361359

362360
assert isinstance(result, dict)
@@ -399,14 +397,14 @@ async def test_read_content_url_encoded_attacks(self, client):
399397
for attack_path in encoded_attacks:
400398
try:
401399
result = await read_content.fn(path=attack_path)
402-
400+
403401
# These may or may not be blocked depending on URL decoding,
404402
# but should not cause security issues
405403
assert isinstance(result, dict)
406-
404+
407405
# If not blocked by security validation, may fail at API level
408406
# which is also acceptable
409-
407+
410408
except Exception:
411409
# Exception due to API failure or other issues is acceptable
412410
# as long as no actual traversal occurs
@@ -435,7 +433,7 @@ async def test_read_content_very_long_attack_path(self, client):
435433
"""Test handling of very long attack paths."""
436434
# Create a very long path traversal attack
437435
long_attack = "../" * 1000 + "etc/passwd"
438-
436+
439437
result = await read_content.fn(path=long_attack)
440438

441439
assert isinstance(result, dict)
@@ -458,4 +456,4 @@ async def test_read_content_case_variations_attacks(self, client):
458456

459457
assert isinstance(result, dict)
460458
assert result["type"] == "error"
461-
assert "paths must stay within project boundaries" in result["error"]
459+
assert "paths must stay within project boundaries" in result["error"]

tests/mcp/test_tool_read_note.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,9 @@ async def test_read_note_allows_safe_identifiers(self, app):
450450

451451
assert isinstance(result, str)
452452
# Should not contain security error message
453-
assert "# Error" not in result or "paths must stay within project boundaries" not in result
453+
assert (
454+
"# Error" not in result or "paths must stay within project boundaries" not in result
455+
)
454456
# Should either succeed or fail for legitimate reasons (not found, etc.)
455457
# but not due to security validation
456458

@@ -466,7 +468,7 @@ async def test_read_note_allows_legitimate_titles(self, app):
466468

467469
# Test reading by title (should work)
468470
result = await read_note.fn("Security Test Note")
469-
471+
470472
assert isinstance(result, str)
471473
# Should not be a security error
472474
assert "# Error" not in result or "paths must stay within project boundaries" not in result
@@ -506,7 +508,7 @@ async def test_read_note_security_logging(self, app, caplog):
506508

507509
assert "# Error" in result
508510
assert "paths must stay within project boundaries" in result
509-
511+
510512
# Check that security violation was logged
511513
# Note: This test may need adjustment based on the actual logging setup
512514
# The security validation should generate a warning log entry
@@ -539,7 +541,7 @@ async def test_read_note_preserves_functionality_with_security(self, app):
539541

540542
# Test reading by permalink
541543
result = await read_note.fn("security-tests/full-feature-security-test-note")
542-
544+
543545
# Should succeed normally (not a security error)
544546
assert isinstance(result, str)
545547
assert "# Error" not in result or "paths must stay within project boundaries" not in result
@@ -571,7 +573,7 @@ async def test_read_note_very_long_attack_identifier(self, app):
571573
"""Test handling of very long attack identifiers."""
572574
# Create a very long path traversal attack
573575
long_attack_identifier = "../" * 1000 + "etc/malicious"
574-
576+
575577
result = await read_note.fn(identifier=long_attack_identifier)
576578

577579
assert isinstance(result, str)

tests/mcp/test_tool_write_note.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,7 @@ async def test_write_note_current_directory_references_security(self, app):
864864
# Test current directory references (should be safe)
865865
safe_folders = [
866866
"./notes",
867-
"folder/./subfolder",
867+
"folder/./subfolder",
868868
"./folder/subfolder",
869869
]
870870

@@ -912,7 +912,7 @@ async def test_write_note_security_logging(self, app, caplog):
912912

913913
assert "# Error" in result
914914
assert "paths must stay within project boundaries" in result
915-
915+
916916
# Check that security violation was logged
917917
# Note: This test may need adjustment based on the actual logging setup
918918
# The security validation should generate a warning log entry
@@ -950,16 +950,16 @@ async def test_write_note_preserves_functionality_with_security(self, app):
950950
assert "# Created note" in result
951951
assert "file_path: security-tests/Full Feature Security Test.md" in result
952952
assert "permalink: security-tests/full-feature-security-test" in result
953-
953+
954954
# Should process observations and relations
955955
assert "## Observations" in result
956956
assert "## Relations" in result
957957
assert "## Tags" in result
958-
958+
959959
# Should show proper counts
960960
assert "security: 1" in result
961961
assert "feature: 1" in result
962-
962+
963963

964964
class TestWriteNoteSecurityEdgeCases:
965965
"""Test edge cases for write_note security validation."""
@@ -990,7 +990,7 @@ async def test_write_note_very_long_attack_folder(self, app):
990990
"""Test handling of very long attack folder paths."""
991991
# Create a very long path traversal attack
992992
long_attack_folder = "../" * 1000 + "etc/malicious"
993-
993+
994994
result = await write_note.fn(
995995
title="Long Attack Test",
996996
folder=long_attack_folder,

0 commit comments

Comments
 (0)