spatial-memory-mcp 1.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. spatial_memory/__init__.py +97 -0
  2. spatial_memory/__main__.py +271 -0
  3. spatial_memory/adapters/__init__.py +7 -0
  4. spatial_memory/adapters/lancedb_repository.py +880 -0
  5. spatial_memory/config.py +769 -0
  6. spatial_memory/core/__init__.py +118 -0
  7. spatial_memory/core/cache.py +317 -0
  8. spatial_memory/core/circuit_breaker.py +297 -0
  9. spatial_memory/core/connection_pool.py +220 -0
  10. spatial_memory/core/consolidation_strategies.py +401 -0
  11. spatial_memory/core/database.py +3072 -0
  12. spatial_memory/core/db_idempotency.py +242 -0
  13. spatial_memory/core/db_indexes.py +576 -0
  14. spatial_memory/core/db_migrations.py +588 -0
  15. spatial_memory/core/db_search.py +512 -0
  16. spatial_memory/core/db_versioning.py +178 -0
  17. spatial_memory/core/embeddings.py +558 -0
  18. spatial_memory/core/errors.py +317 -0
  19. spatial_memory/core/file_security.py +701 -0
  20. spatial_memory/core/filesystem.py +178 -0
  21. spatial_memory/core/health.py +289 -0
  22. spatial_memory/core/helpers.py +79 -0
  23. spatial_memory/core/import_security.py +433 -0
  24. spatial_memory/core/lifecycle_ops.py +1067 -0
  25. spatial_memory/core/logging.py +194 -0
  26. spatial_memory/core/metrics.py +192 -0
  27. spatial_memory/core/models.py +660 -0
  28. spatial_memory/core/rate_limiter.py +326 -0
  29. spatial_memory/core/response_types.py +500 -0
  30. spatial_memory/core/security.py +588 -0
  31. spatial_memory/core/spatial_ops.py +430 -0
  32. spatial_memory/core/tracing.py +300 -0
  33. spatial_memory/core/utils.py +110 -0
  34. spatial_memory/core/validation.py +406 -0
  35. spatial_memory/factory.py +444 -0
  36. spatial_memory/migrations/__init__.py +40 -0
  37. spatial_memory/ports/__init__.py +11 -0
  38. spatial_memory/ports/repositories.py +630 -0
  39. spatial_memory/py.typed +0 -0
  40. spatial_memory/server.py +1214 -0
  41. spatial_memory/services/__init__.py +70 -0
  42. spatial_memory/services/decay_manager.py +411 -0
  43. spatial_memory/services/export_import.py +1031 -0
  44. spatial_memory/services/lifecycle.py +1139 -0
  45. spatial_memory/services/memory.py +412 -0
  46. spatial_memory/services/spatial.py +1152 -0
  47. spatial_memory/services/utility.py +429 -0
  48. spatial_memory/tools/__init__.py +5 -0
  49. spatial_memory/tools/definitions.py +695 -0
  50. spatial_memory/verify.py +140 -0
  51. spatial_memory_mcp-1.9.1.dist-info/METADATA +509 -0
  52. spatial_memory_mcp-1.9.1.dist-info/RECORD +55 -0
  53. spatial_memory_mcp-1.9.1.dist-info/WHEEL +4 -0
  54. spatial_memory_mcp-1.9.1.dist-info/entry_points.txt +2 -0
  55. spatial_memory_mcp-1.9.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,588 @@
1
+ """Consolidated security module for file path operations.
2
+
3
+ This module provides a unified security facade for all file path validation
4
+ operations in the spatial-memory-mcp project. It consolidates path traversal
5
+ prevention, symlink attack prevention, file size limits, and allowed directory
6
+ configuration into a single, production-ready interface.
7
+
8
+ Security Architecture:
9
+ ----------------------
10
+ The security system implements defense-in-depth with multiple layers:
11
+
12
+ 1. **Input Validation Layer**
13
+ - Pattern-based detection of known attack vectors
14
+ - URL decoding to catch encoded attacks
15
+ - Null byte injection prevention
16
+
17
+ 2. **Path Canonicalization Layer**
18
+ - Resolution of symbolic path elements
19
+ - Detection of traversal after normalization
20
+ - UNC path blocking (Windows network shares)
21
+
22
+ 3. **Access Control Layer**
23
+ - Directory allowlist enforcement
24
+ - Sensitive directory blocking
25
+ - File extension validation
26
+
27
+ 4. **Runtime Protection Layer**
28
+ - Symlink detection and optional blocking
29
+ - File size limit enforcement
30
+ - TOCTOU (Time-of-Check-Time-of-Use) prevention
31
+
32
+ Threat Model:
33
+ -------------
34
+ This module defends against:
35
+
36
+ - **Path Traversal Attacks**: Attempts to access files outside allowed
37
+ directories using sequences like ../, URL encoding (%2e%2e), or
38
+ double encoding (%252e%252e).
39
+
40
+ - **Symlink Attacks**: Creating symlinks in allowed directories that
41
+ point to sensitive files elsewhere.
42
+
43
+ - **UNC Path Attacks**: Accessing network shares on Windows using
44
+ paths like \\\\server\\share.
45
+
46
+ - **Large File DoS**: Uploading excessively large files to exhaust
47
+ disk space or memory.
48
+
49
+ - **Extension Spoofing**: Attempting to import/export executable or
50
+ script files by disguising extensions.
51
+
52
+ - **TOCTOU Race Conditions**: Swapping files between validation and
53
+ actual file operations.
54
+
55
+ Usage Example:
56
+ --------------
57
+ from spatial_memory.core.security import (
58
+ FileSecurityManager,
59
+ SecurityConfig,
60
+ validate_export_path,
61
+ validate_import_file,
62
+ )
63
+
64
+ # Using the high-level API
65
+ config = SecurityConfig(
66
+ export_allowed_paths=["./exports", "./backups"],
67
+ import_allowed_paths=["./imports"],
68
+ max_import_size_mb=100.0,
69
+ allow_symlinks=False,
70
+ )
71
+ manager = FileSecurityManager(config)
72
+
73
+ # Validate export path
74
+ safe_path = manager.validate_export_path("./exports/backup.parquet")
75
+
76
+ # Validate and open import file atomically (TOCTOU-safe)
77
+ path, handle = manager.validate_and_open_import("./imports/data.json")
78
+ try:
79
+ content = handle.read()
80
+ finally:
81
+ handle.close()
82
+
83
+ # Or use convenience functions with default settings
84
+ safe_path = validate_export_path("./exports/backup.parquet", config)
85
+
86
+ Module Dependencies:
87
+ --------------------
88
+ - spatial_memory.core.errors: Custom exception types
89
+ - spatial_memory.core.file_security: PathValidator implementation
90
+ - spatial_memory.core.import_security: Import record validation
91
+ - spatial_memory.config: Application configuration
92
+
93
+ Author: Spatial Memory MCP Team
94
+ Security Review: Phase 5 Implementation
95
+ """
96
+
97
+ from __future__ import annotations
98
+
99
+ import logging
100
+ from dataclasses import dataclass, field
101
+ from pathlib import Path
102
+ from typing import BinaryIO
103
+
104
+ from spatial_memory.core.errors import (
105
+ DimensionMismatchError,
106
+ FileSizeLimitError,
107
+ PathSecurityError,
108
+ SchemaValidationError,
109
+ ValidationError,
110
+ )
111
+ from spatial_memory.core.file_security import (
112
+ PATH_TRAVERSAL_PATTERNS,
113
+ SENSITIVE_DIRECTORIES,
114
+ VALID_EXTENSIONS,
115
+ PathValidator,
116
+ )
117
+ from spatial_memory.core.import_security import (
118
+ BatchValidationResult,
119
+ ImportValidationConfig,
120
+ ImportValidator,
121
+ )
122
+
123
+ __all__ = [
124
+ # Configuration
125
+ "SecurityConfig",
126
+ "DEFAULT_SECURITY_CONFIG",
127
+ # Manager class
128
+ "FileSecurityManager",
129
+ # Convenience functions
130
+ "validate_export_path",
131
+ "validate_import_path",
132
+ "validate_import_file",
133
+ "validate_import_records",
134
+ # Error types (re-exported for convenience)
135
+ "PathSecurityError",
136
+ "FileSizeLimitError",
137
+ "DimensionMismatchError",
138
+ "SchemaValidationError",
139
+ "ValidationError",
140
+ # Constants (re-exported for inspection)
141
+ "PATH_TRAVERSAL_PATTERNS",
142
+ "SENSITIVE_DIRECTORIES",
143
+ "VALID_EXTENSIONS",
144
+ # Validation result types
145
+ "BatchValidationResult",
146
+ "ImportValidationConfig",
147
+ ]
148
+
149
+ logger = logging.getLogger(__name__)
150
+
151
+
152
+ # =============================================================================
153
+ # Configuration
154
+ # =============================================================================
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class SecurityConfig:
159
+ """Immutable security configuration for file operations.
160
+
161
+ This configuration is intentionally immutable (frozen=True) to prevent
162
+ runtime modification of security settings after initialization.
163
+
164
+ Attributes:
165
+ export_allowed_paths: Directories where exports are permitted.
166
+ Paths can be absolute or relative. Relative paths are resolved
167
+ against the current working directory.
168
+
169
+ import_allowed_paths: Directories where imports are permitted.
170
+ Same resolution rules as export_allowed_paths.
171
+
172
+ max_import_size_mb: Maximum file size for imports in megabytes.
173
+ Files exceeding this size will be rejected to prevent DoS
174
+ attacks through disk/memory exhaustion.
175
+
176
+ allow_symlinks: Whether to allow following symlinks. Default False
177
+ for security - symlinks can escape allowed directories.
178
+
179
+ max_import_records: Maximum number of records in an import file.
180
+ Prevents memory exhaustion from very large files.
181
+
182
+ Example:
183
+ config = SecurityConfig(
184
+ export_allowed_paths=["./exports", "/data/backups"],
185
+ import_allowed_paths=["./imports"],
186
+ max_import_size_mb=100.0,
187
+ allow_symlinks=False,
188
+ max_import_records=100_000,
189
+ )
190
+ """
191
+
192
+ export_allowed_paths: tuple[str, ...] = field(
193
+ default_factory=lambda: ("./exports", "./backups")
194
+ )
195
+ import_allowed_paths: tuple[str, ...] = field(
196
+ default_factory=lambda: ("./imports", "./backups")
197
+ )
198
+ max_import_size_mb: float = 100.0
199
+ allow_symlinks: bool = False
200
+ max_import_records: int = 100_000
201
+
202
+ def __post_init__(self) -> None:
203
+ """Validate configuration values."""
204
+ # Convert lists to tuples if needed (for immutability)
205
+ if isinstance(self.export_allowed_paths, list):
206
+ object.__setattr__(
207
+ self, "export_allowed_paths", tuple(self.export_allowed_paths)
208
+ )
209
+ if isinstance(self.import_allowed_paths, list):
210
+ object.__setattr__(
211
+ self, "import_allowed_paths", tuple(self.import_allowed_paths)
212
+ )
213
+
214
+ # Validate ranges
215
+ if self.max_import_size_mb <= 0:
216
+ raise ValueError("max_import_size_mb must be positive")
217
+ if self.max_import_size_mb > 10_000:
218
+ raise ValueError("max_import_size_mb cannot exceed 10GB")
219
+ if self.max_import_records <= 0:
220
+ raise ValueError("max_import_records must be positive")
221
+
222
+ @property
223
+ def max_import_size_bytes(self) -> int:
224
+ """Return maximum import size in bytes."""
225
+ return int(self.max_import_size_mb * 1024 * 1024)
226
+
227
+
228
+ # Default security configuration for general use
229
+ DEFAULT_SECURITY_CONFIG = SecurityConfig()
230
+
231
+
232
+ # =============================================================================
233
+ # File Security Manager
234
+ # =============================================================================
235
+
236
+
237
+ class FileSecurityManager:
238
+ """Unified security manager for all file path operations.
239
+
240
+ This class provides a thread-safe, production-ready interface for
241
+ validating file paths used in export/import operations. It encapsulates
242
+ all security checks into a single interface.
243
+
244
+ Thread Safety:
245
+ This class is thread-safe. The underlying PathValidator is stateless
246
+ and only reads from immutable configuration.
247
+
248
+ Example:
249
+ manager = FileSecurityManager(config)
250
+
251
+ # Export validation (file doesn't need to exist)
252
+ safe_path = manager.validate_export_path("./exports/backup.parquet")
253
+
254
+ # Import validation (file must exist, size checked)
255
+ safe_path = manager.validate_import_path("./imports/data.json")
256
+
257
+ # TOCTOU-safe import (validates and opens atomically)
258
+ path, handle = manager.validate_and_open_import("./imports/data.json")
259
+ try:
260
+ data = handle.read()
261
+ finally:
262
+ handle.close()
263
+ """
264
+
265
+ def __init__(self, config: SecurityConfig | None = None) -> None:
266
+ """Initialize the security manager.
267
+
268
+ Args:
269
+ config: Security configuration. Uses DEFAULT_SECURITY_CONFIG
270
+ if not provided.
271
+ """
272
+ self._config = config or DEFAULT_SECURITY_CONFIG
273
+
274
+ # Initialize the underlying PathValidator
275
+ self._validator = PathValidator(
276
+ allowed_export_paths=list(self._config.export_allowed_paths),
277
+ allowed_import_paths=list(self._config.import_allowed_paths),
278
+ allow_symlinks=self._config.allow_symlinks,
279
+ )
280
+
281
+ logger.debug(
282
+ "FileSecurityManager initialized with config: "
283
+ f"export_paths={self._config.export_allowed_paths}, "
284
+ f"import_paths={self._config.import_allowed_paths}, "
285
+ f"max_size_mb={self._config.max_import_size_mb}, "
286
+ f"allow_symlinks={self._config.allow_symlinks}"
287
+ )
288
+
289
+ @property
290
+ def config(self) -> SecurityConfig:
291
+ """Return the security configuration (read-only)."""
292
+ return self._config
293
+
294
+ def validate_export_path(self, path: str | Path) -> Path:
295
+ """Validate a path for export operations.
296
+
297
+ Performs all security checks without requiring the file to exist.
298
+ Parent directories will be created during export if needed.
299
+
300
+ Security checks performed:
301
+ 1. Path traversal pattern detection
302
+ 2. URL decoding and re-checking
303
+ 3. UNC path blocking
304
+ 4. Path canonicalization
305
+ 5. Extension validation (.parquet, .json, .csv only)
306
+ 6. Symlink detection (if file exists and symlinks disabled)
307
+ 7. Allowlist validation
308
+
309
+ Args:
310
+ path: The path to validate. Can be absolute or relative.
311
+
312
+ Returns:
313
+ Canonicalized Path object that is safe to use.
314
+
315
+ Raises:
316
+ PathSecurityError: If the path fails any security check.
317
+ ValueError: If the path is empty or contains null bytes.
318
+
319
+ Example:
320
+ safe_path = manager.validate_export_path("./exports/backup.parquet")
321
+ # Use safe_path for actual file writing
322
+ """
323
+ return self._validator.validate_export_path(path)
324
+
325
+ def validate_import_path(self, path: str | Path) -> Path:
326
+ """Validate a path for import operations.
327
+
328
+ Performs all export validation checks plus:
329
+ - File existence verification
330
+ - Directory rejection (must be a file)
331
+ - File size limit enforcement
332
+
333
+ Args:
334
+ path: The path to validate. Can be absolute or relative.
335
+
336
+ Returns:
337
+ Canonicalized Path object that is safe to use.
338
+
339
+ Raises:
340
+ PathSecurityError: If the path fails any security check.
341
+ FileSizeLimitError: If the file exceeds the size limit.
342
+ ValueError: If the path is empty or contains null bytes.
343
+
344
+ Example:
345
+ safe_path = manager.validate_import_path("./imports/data.json")
346
+ # Use safe_path for actual file reading
347
+ """
348
+ return self._validator.validate_import_path(
349
+ path, max_size_bytes=self._config.max_import_size_bytes
350
+ )
351
+
352
+ def validate_and_open_import(
353
+ self, path: str | Path
354
+ ) -> tuple[Path, BinaryIO]:
355
+ """Atomically validate and open a file for import.
356
+
357
+ This method prevents TOCTOU (Time-of-Check-Time-of-Use) race
358
+ conditions by opening the file FIRST, then validating properties
359
+ on the open file descriptor.
360
+
361
+ IMPORTANT: The caller MUST use the returned file handle for reading.
362
+ DO NOT re-open the file by path after this call.
363
+
364
+ Args:
365
+ path: The path to validate and open.
366
+
367
+ Returns:
368
+ Tuple of (canonical_path, file_handle). The file handle is
369
+ opened in binary read mode ('rb'). Caller is responsible
370
+ for closing it.
371
+
372
+ Raises:
373
+ PathSecurityError: If the path fails any security check.
374
+ FileSizeLimitError: If the file exceeds the size limit.
375
+ ValueError: If the path is empty or contains null bytes.
376
+
377
+ Example:
378
+ path, handle = manager.validate_and_open_import("./imports/data.json")
379
+ try:
380
+ data = handle.read()
381
+ # Process data...
382
+ finally:
383
+ handle.close()
384
+ """
385
+ return self._validator.validate_and_open_import_file(
386
+ path, max_size_bytes=self._config.max_import_size_bytes
387
+ )
388
+
389
+ def validate_import_records(
390
+ self,
391
+ records: list[dict[str, object]],
392
+ expected_vector_dim: int,
393
+ fail_fast: bool = False,
394
+ ) -> BatchValidationResult:
395
+ """Validate a batch of import records for schema compliance.
396
+
397
+ Validates each record against the import schema:
398
+ - Required fields (content)
399
+ - Optional field types (namespace, tags, importance, metadata, vector)
400
+ - Vector dimension matching
401
+ - Value constraints (importance range, etc.)
402
+
403
+ Args:
404
+ records: List of record dictionaries to validate.
405
+ expected_vector_dim: Expected vector dimensions.
406
+ fail_fast: If True, stop on first error.
407
+
408
+ Returns:
409
+ BatchValidationResult with validation statistics and errors.
410
+
411
+ Example:
412
+ result = manager.validate_import_records(records, expected_dim=384)
413
+ if not result.is_valid:
414
+ for idx, errors in result.errors:
415
+ print(f"Record {idx} errors: {errors}")
416
+ """
417
+ config = ImportValidationConfig(
418
+ expected_vector_dim=expected_vector_dim,
419
+ fail_fast=fail_fast,
420
+ max_errors=100,
421
+ )
422
+ validator = ImportValidator(config)
423
+ return validator.validate_batch(records, expected_vector_dim)
424
+
425
+ def is_path_safe(self, path: str | Path, operation: str = "export") -> bool:
426
+ """Check if a path passes security validation.
427
+
428
+ Convenience method that returns a boolean instead of raising.
429
+ Useful for pre-flight checks in UI code.
430
+
431
+ Args:
432
+ path: The path to check.
433
+ operation: Either "export" or "import".
434
+
435
+ Returns:
436
+ True if the path passes all security checks, False otherwise.
437
+
438
+ Example:
439
+ if manager.is_path_safe(user_input, "export"):
440
+ # Proceed with export
441
+ pass
442
+ else:
443
+ # Show error to user
444
+ pass
445
+ """
446
+ try:
447
+ if operation == "export":
448
+ self.validate_export_path(path)
449
+ elif operation == "import":
450
+ self.validate_import_path(path)
451
+ else:
452
+ raise ValueError(f"Unknown operation: {operation}")
453
+ return True
454
+ except (PathSecurityError, FileSizeLimitError, ValueError):
455
+ return False
456
+
457
+
458
+ # =============================================================================
459
+ # Convenience Functions
460
+ # =============================================================================
461
+
462
+
463
+ def validate_export_path(
464
+ path: str | Path,
465
+ config: SecurityConfig | None = None,
466
+ ) -> Path:
467
+ """Convenience function to validate an export path.
468
+
469
+ Creates a FileSecurityManager with the provided config and validates
470
+ the path. For repeated validations, prefer creating a manager instance.
471
+
472
+ Args:
473
+ path: The path to validate.
474
+ config: Security configuration (uses defaults if not provided).
475
+
476
+ Returns:
477
+ Canonicalized Path object that is safe to use.
478
+
479
+ Raises:
480
+ PathSecurityError: If the path fails security checks.
481
+ """
482
+ manager = FileSecurityManager(config)
483
+ return manager.validate_export_path(path)
484
+
485
+
486
+ def validate_import_path(
487
+ path: str | Path,
488
+ config: SecurityConfig | None = None,
489
+ ) -> Path:
490
+ """Convenience function to validate an import path.
491
+
492
+ Creates a FileSecurityManager with the provided config and validates
493
+ the path. For repeated validations, prefer creating a manager instance.
494
+
495
+ Args:
496
+ path: The path to validate.
497
+ config: Security configuration (uses defaults if not provided).
498
+
499
+ Returns:
500
+ Canonicalized Path object that is safe to use.
501
+
502
+ Raises:
503
+ PathSecurityError: If the path fails security checks.
504
+ FileSizeLimitError: If the file exceeds size limits.
505
+ """
506
+ manager = FileSecurityManager(config)
507
+ return manager.validate_import_path(path)
508
+
509
+
510
+ def validate_import_file(
511
+ path: str | Path,
512
+ config: SecurityConfig | None = None,
513
+ ) -> tuple[Path, BinaryIO]:
514
+ """Convenience function to validate and open an import file atomically.
515
+
516
+ This is the TOCTOU-safe way to open import files. The returned file
517
+ handle MUST be used for reading - do not re-open by path.
518
+
519
+ Args:
520
+ path: The path to validate and open.
521
+ config: Security configuration (uses defaults if not provided).
522
+
523
+ Returns:
524
+ Tuple of (canonical_path, file_handle).
525
+
526
+ Raises:
527
+ PathSecurityError: If the path fails security checks.
528
+ FileSizeLimitError: If the file exceeds size limits.
529
+ """
530
+ manager = FileSecurityManager(config)
531
+ return manager.validate_and_open_import(path)
532
+
533
+
534
+ def validate_import_records(
535
+ records: list[dict[str, object]],
536
+ expected_vector_dim: int,
537
+ fail_fast: bool = False,
538
+ ) -> BatchValidationResult:
539
+ """Convenience function to validate import records.
540
+
541
+ Args:
542
+ records: List of record dictionaries to validate.
543
+ expected_vector_dim: Expected vector dimensions.
544
+ fail_fast: If True, stop on first error.
545
+
546
+ Returns:
547
+ BatchValidationResult with validation statistics.
548
+ """
549
+ config = ImportValidationConfig(
550
+ expected_vector_dim=expected_vector_dim,
551
+ fail_fast=fail_fast,
552
+ max_errors=100,
553
+ )
554
+ validator = ImportValidator(config)
555
+ return validator.validate_batch(records, expected_vector_dim)
556
+
557
+
558
+ # =============================================================================
559
+ # Factory Function for Settings Integration
560
+ # =============================================================================
561
+
562
+
563
+ def create_security_manager_from_settings() -> FileSecurityManager:
564
+ """Create a FileSecurityManager from application settings.
565
+
566
+ Reads security configuration from the application Settings object
567
+ (environment variables or .env file).
568
+
569
+ Returns:
570
+ Configured FileSecurityManager instance.
571
+
572
+ Example:
573
+ manager = create_security_manager_from_settings()
574
+ safe_path = manager.validate_export_path(user_path)
575
+ """
576
+ from spatial_memory.config import get_settings
577
+
578
+ settings = get_settings()
579
+
580
+ config = SecurityConfig(
581
+ export_allowed_paths=tuple(settings.export_allowed_paths),
582
+ import_allowed_paths=tuple(settings.import_allowed_paths),
583
+ max_import_size_mb=settings.import_max_file_size_mb,
584
+ allow_symlinks=settings.export_allow_symlinks or settings.import_allow_symlinks,
585
+ max_import_records=settings.import_max_records,
586
+ )
587
+
588
+ return FileSecurityManager(config)