spatial-memory-mcp 1.6.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.
Potentially problematic release.
This version of spatial-memory-mcp might be problematic. Click here for more details.
- spatial_memory/__init__.py +97 -0
- spatial_memory/__main__.py +270 -0
- spatial_memory/adapters/__init__.py +7 -0
- spatial_memory/adapters/lancedb_repository.py +878 -0
- spatial_memory/config.py +728 -0
- spatial_memory/core/__init__.py +118 -0
- spatial_memory/core/cache.py +317 -0
- spatial_memory/core/circuit_breaker.py +297 -0
- spatial_memory/core/connection_pool.py +220 -0
- spatial_memory/core/consolidation_strategies.py +402 -0
- spatial_memory/core/database.py +3069 -0
- spatial_memory/core/db_idempotency.py +242 -0
- spatial_memory/core/db_indexes.py +575 -0
- spatial_memory/core/db_migrations.py +584 -0
- spatial_memory/core/db_search.py +509 -0
- spatial_memory/core/db_versioning.py +177 -0
- spatial_memory/core/embeddings.py +557 -0
- spatial_memory/core/errors.py +317 -0
- spatial_memory/core/file_security.py +702 -0
- spatial_memory/core/filesystem.py +178 -0
- spatial_memory/core/health.py +289 -0
- spatial_memory/core/helpers.py +79 -0
- spatial_memory/core/import_security.py +432 -0
- spatial_memory/core/lifecycle_ops.py +1067 -0
- spatial_memory/core/logging.py +194 -0
- spatial_memory/core/metrics.py +192 -0
- spatial_memory/core/models.py +628 -0
- spatial_memory/core/rate_limiter.py +326 -0
- spatial_memory/core/response_types.py +497 -0
- spatial_memory/core/security.py +588 -0
- spatial_memory/core/spatial_ops.py +426 -0
- spatial_memory/core/tracing.py +300 -0
- spatial_memory/core/utils.py +110 -0
- spatial_memory/core/validation.py +403 -0
- spatial_memory/factory.py +407 -0
- spatial_memory/migrations/__init__.py +40 -0
- spatial_memory/ports/__init__.py +11 -0
- spatial_memory/ports/repositories.py +631 -0
- spatial_memory/py.typed +0 -0
- spatial_memory/server.py +1141 -0
- spatial_memory/services/__init__.py +70 -0
- spatial_memory/services/export_import.py +1023 -0
- spatial_memory/services/lifecycle.py +1120 -0
- spatial_memory/services/memory.py +412 -0
- spatial_memory/services/spatial.py +1147 -0
- spatial_memory/services/utility.py +409 -0
- spatial_memory/tools/__init__.py +5 -0
- spatial_memory/tools/definitions.py +695 -0
- spatial_memory/verify.py +140 -0
- spatial_memory_mcp-1.6.1.dist-info/METADATA +499 -0
- spatial_memory_mcp-1.6.1.dist-info/RECORD +54 -0
- spatial_memory_mcp-1.6.1.dist-info/WHEEL +4 -0
- spatial_memory_mcp-1.6.1.dist-info/entry_points.txt +2 -0
- spatial_memory_mcp-1.6.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Import validation module for secure memory import operations.
|
|
2
|
+
|
|
3
|
+
Provides validation for import records to ensure data integrity and
|
|
4
|
+
security during bulk import operations.
|
|
5
|
+
|
|
6
|
+
Classes:
|
|
7
|
+
ImportValidationConfig: Configuration for import validation.
|
|
8
|
+
ImportValidator: Validates import records against schema and constraints.
|
|
9
|
+
BatchValidationResult: Result of batch validation operation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Validation Patterns
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
# Namespace must be alphanumeric with hyphens and underscores only
|
|
23
|
+
# No path traversal characters: /, \, .., ;
|
|
24
|
+
NAMESPACE_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
|
|
25
|
+
|
|
26
|
+
# Invalid namespace patterns for security
|
|
27
|
+
INVALID_NAMESPACE_PATTERNS = [
|
|
28
|
+
re.compile(r"\.\."), # Path traversal
|
|
29
|
+
re.compile(r"[/\\]"), # Directory separators
|
|
30
|
+
re.compile(r"[;]"), # SQL injection risk
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Configuration
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ImportValidationConfig:
|
|
41
|
+
"""Configuration for import validation.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
expected_vector_dim: Expected vector dimensions for embeddings.
|
|
45
|
+
fail_fast: If True, stop validation on first error.
|
|
46
|
+
max_errors: Maximum number of errors to collect per record.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
expected_vector_dim: int
|
|
50
|
+
fail_fast: bool = False
|
|
51
|
+
max_errors: int = 100
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Result Types
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class BatchValidationResult:
|
|
61
|
+
"""Result of batch validation operation.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
valid_count: Number of valid records.
|
|
65
|
+
invalid_count: Number of invalid records.
|
|
66
|
+
errors: List of (record_index, error_messages) tuples.
|
|
67
|
+
is_valid: True if all records are valid.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
valid_count: int = 0
|
|
71
|
+
invalid_count: int = 0
|
|
72
|
+
errors: list[tuple[int, list[str]]] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_valid(self) -> bool:
|
|
76
|
+
"""Return True if all records are valid."""
|
|
77
|
+
return self.invalid_count == 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# =============================================================================
|
|
81
|
+
# ImportValidator
|
|
82
|
+
# =============================================================================
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ImportValidator:
|
|
86
|
+
"""Validates import records for schema and constraint compliance.
|
|
87
|
+
|
|
88
|
+
Validates individual records or batches of records against:
|
|
89
|
+
- Required fields (content)
|
|
90
|
+
- Optional field types (namespace, tags, importance, metadata, vector)
|
|
91
|
+
- Vector dimension matching
|
|
92
|
+
- Value constraints (importance range, etc.)
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> config = ImportValidationConfig(expected_vector_dim=384)
|
|
96
|
+
>>> validator = ImportValidator(config)
|
|
97
|
+
>>> is_valid, errors = validator.validate_record(record, 384, 0)
|
|
98
|
+
>>> if not is_valid:
|
|
99
|
+
... for error in errors:
|
|
100
|
+
... print(f"Validation error: {error}")
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, config: ImportValidationConfig) -> None:
|
|
104
|
+
"""Initialize validator with configuration.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
config: Validation configuration.
|
|
108
|
+
"""
|
|
109
|
+
self.config = config
|
|
110
|
+
|
|
111
|
+
def validate_record(
|
|
112
|
+
self,
|
|
113
|
+
record: dict[str, Any],
|
|
114
|
+
expected_vector_dim: int,
|
|
115
|
+
record_index: int,
|
|
116
|
+
) -> tuple[bool, list[str]]:
|
|
117
|
+
"""Validate a single import record.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
record: The record dictionary to validate.
|
|
121
|
+
expected_vector_dim: Expected vector dimensions.
|
|
122
|
+
record_index: Index of record in batch (for error messages).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Tuple of (is_valid, list_of_errors).
|
|
126
|
+
is_valid is True if record passes all validation.
|
|
127
|
+
list_of_errors contains error messages for failures.
|
|
128
|
+
"""
|
|
129
|
+
errors: list[str] = []
|
|
130
|
+
|
|
131
|
+
# Validate required fields first
|
|
132
|
+
self._validate_content(record, record_index, errors)
|
|
133
|
+
|
|
134
|
+
# Early exit if fail_fast and already have errors
|
|
135
|
+
if self.config.fail_fast and errors:
|
|
136
|
+
return False, errors
|
|
137
|
+
|
|
138
|
+
# Validate optional fields
|
|
139
|
+
if self._should_continue(errors):
|
|
140
|
+
self._validate_namespace(record, record_index, errors)
|
|
141
|
+
|
|
142
|
+
if self._should_continue(errors):
|
|
143
|
+
self._validate_tags(record, record_index, errors)
|
|
144
|
+
|
|
145
|
+
if self._should_continue(errors):
|
|
146
|
+
self._validate_importance(record, record_index, errors)
|
|
147
|
+
|
|
148
|
+
if self._should_continue(errors):
|
|
149
|
+
self._validate_metadata(record, record_index, errors)
|
|
150
|
+
|
|
151
|
+
if self._should_continue(errors):
|
|
152
|
+
self._validate_vector(record, expected_vector_dim, record_index, errors)
|
|
153
|
+
|
|
154
|
+
is_valid = len(errors) == 0
|
|
155
|
+
return is_valid, errors
|
|
156
|
+
|
|
157
|
+
def validate_batch(
|
|
158
|
+
self,
|
|
159
|
+
records: list[dict[str, Any]],
|
|
160
|
+
expected_vector_dim: int,
|
|
161
|
+
) -> BatchValidationResult:
|
|
162
|
+
"""Validate a batch of import records.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
records: List of records to validate.
|
|
166
|
+
expected_vector_dim: Expected vector dimensions.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
BatchValidationResult with counts and errors.
|
|
170
|
+
"""
|
|
171
|
+
result = BatchValidationResult()
|
|
172
|
+
|
|
173
|
+
for index, record in enumerate(records):
|
|
174
|
+
is_valid, errors = self.validate_record(
|
|
175
|
+
record, expected_vector_dim, index
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if is_valid:
|
|
179
|
+
result.valid_count += 1
|
|
180
|
+
else:
|
|
181
|
+
result.invalid_count += 1
|
|
182
|
+
result.errors.append((index, errors))
|
|
183
|
+
|
|
184
|
+
# Fail fast for batch
|
|
185
|
+
if self.config.fail_fast:
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def _should_continue(self, errors: list[str]) -> bool:
|
|
191
|
+
"""Check if validation should continue.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
errors: Current error list.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if validation should continue.
|
|
198
|
+
"""
|
|
199
|
+
if self.config.fail_fast and errors:
|
|
200
|
+
return False
|
|
201
|
+
if len(errors) >= self.config.max_errors:
|
|
202
|
+
return False
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def _validate_content(
|
|
206
|
+
self,
|
|
207
|
+
record: dict[str, Any],
|
|
208
|
+
record_index: int,
|
|
209
|
+
errors: list[str],
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Validate the content field (required).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
record: Record to validate.
|
|
215
|
+
record_index: Index for error messages.
|
|
216
|
+
errors: List to append errors to.
|
|
217
|
+
"""
|
|
218
|
+
content = record.get("content")
|
|
219
|
+
|
|
220
|
+
# Check if content is missing
|
|
221
|
+
if content is None or "content" not in record:
|
|
222
|
+
errors.append(
|
|
223
|
+
f"Record {record_index}: 'content' field is required"
|
|
224
|
+
)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Check if content is a string
|
|
228
|
+
if not isinstance(content, str):
|
|
229
|
+
errors.append(
|
|
230
|
+
f"Record {record_index}: 'content' must be a string, "
|
|
231
|
+
f"got {type(content).__name__}"
|
|
232
|
+
)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
# Check if content is non-empty (after stripping whitespace)
|
|
236
|
+
if not content.strip():
|
|
237
|
+
errors.append(
|
|
238
|
+
f"Record {record_index}: 'content' must be non-empty"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _validate_namespace(
|
|
242
|
+
self,
|
|
243
|
+
record: dict[str, Any],
|
|
244
|
+
record_index: int,
|
|
245
|
+
errors: list[str],
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Validate the namespace field (optional).
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
record: Record to validate.
|
|
251
|
+
record_index: Index for error messages.
|
|
252
|
+
errors: List to append errors to.
|
|
253
|
+
"""
|
|
254
|
+
namespace = record.get("namespace")
|
|
255
|
+
|
|
256
|
+
# Namespace is optional
|
|
257
|
+
if namespace is None:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Check type
|
|
261
|
+
if not isinstance(namespace, str):
|
|
262
|
+
errors.append(
|
|
263
|
+
f"Record {record_index}: 'namespace' must be a string, "
|
|
264
|
+
f"got {type(namespace).__name__}"
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Check empty
|
|
269
|
+
if not namespace:
|
|
270
|
+
errors.append(
|
|
271
|
+
f"Record {record_index}: 'namespace' cannot be empty string"
|
|
272
|
+
)
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
# Check for invalid patterns (security)
|
|
276
|
+
for pattern in INVALID_NAMESPACE_PATTERNS:
|
|
277
|
+
if pattern.search(namespace):
|
|
278
|
+
errors.append(
|
|
279
|
+
f"Record {record_index}: 'namespace' contains invalid characters: "
|
|
280
|
+
f"'{namespace}'"
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Check valid format
|
|
285
|
+
if not NAMESPACE_PATTERN.match(namespace):
|
|
286
|
+
errors.append(
|
|
287
|
+
f"Record {record_index}: 'namespace' has invalid format: "
|
|
288
|
+
f"'{namespace}' (must be alphanumeric with hyphens/underscores)"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def _validate_tags(
|
|
292
|
+
self,
|
|
293
|
+
record: dict[str, Any],
|
|
294
|
+
record_index: int,
|
|
295
|
+
errors: list[str],
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Validate the tags field (optional).
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
record: Record to validate.
|
|
301
|
+
record_index: Index for error messages.
|
|
302
|
+
errors: List to append errors to.
|
|
303
|
+
"""
|
|
304
|
+
tags = record.get("tags")
|
|
305
|
+
|
|
306
|
+
# Tags are optional
|
|
307
|
+
if tags is None:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# Check type is list
|
|
311
|
+
if not isinstance(tags, list):
|
|
312
|
+
errors.append(
|
|
313
|
+
f"Record {record_index}: 'tags' must be a list, "
|
|
314
|
+
f"got {type(tags).__name__}"
|
|
315
|
+
)
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Check all items are strings
|
|
319
|
+
for i, tag in enumerate(tags):
|
|
320
|
+
if not isinstance(tag, str):
|
|
321
|
+
errors.append(
|
|
322
|
+
f"Record {record_index}: 'tags[{i}]' must be a string, "
|
|
323
|
+
f"got {type(tag).__name__}"
|
|
324
|
+
)
|
|
325
|
+
return # One error for tags is enough
|
|
326
|
+
|
|
327
|
+
def _validate_importance(
|
|
328
|
+
self,
|
|
329
|
+
record: dict[str, Any],
|
|
330
|
+
record_index: int,
|
|
331
|
+
errors: list[str],
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Validate the importance field (optional).
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
record: Record to validate.
|
|
337
|
+
record_index: Index for error messages.
|
|
338
|
+
errors: List to append errors to.
|
|
339
|
+
"""
|
|
340
|
+
importance = record.get("importance")
|
|
341
|
+
|
|
342
|
+
# Importance is optional
|
|
343
|
+
if importance is None:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# Check type (allow int or float)
|
|
347
|
+
if not isinstance(importance, (int, float)):
|
|
348
|
+
errors.append(
|
|
349
|
+
f"Record {record_index}: 'importance' must be a number, "
|
|
350
|
+
f"got {type(importance).__name__}"
|
|
351
|
+
)
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
# Check range [0, 1]
|
|
355
|
+
if importance < 0 or importance > 1:
|
|
356
|
+
errors.append(
|
|
357
|
+
f"Record {record_index}: 'importance' must be between 0 and 1, "
|
|
358
|
+
f"got {importance}"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _validate_metadata(
|
|
362
|
+
self,
|
|
363
|
+
record: dict[str, Any],
|
|
364
|
+
record_index: int,
|
|
365
|
+
errors: list[str],
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Validate the metadata field (optional).
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
record: Record to validate.
|
|
371
|
+
record_index: Index for error messages.
|
|
372
|
+
errors: List to append errors to.
|
|
373
|
+
"""
|
|
374
|
+
metadata = record.get("metadata")
|
|
375
|
+
|
|
376
|
+
# Metadata is optional
|
|
377
|
+
if metadata is None:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Check type is dict
|
|
381
|
+
if not isinstance(metadata, dict):
|
|
382
|
+
errors.append(
|
|
383
|
+
f"Record {record_index}: 'metadata' must be a dict, "
|
|
384
|
+
f"got {type(metadata).__name__}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def _validate_vector(
|
|
388
|
+
self,
|
|
389
|
+
record: dict[str, Any],
|
|
390
|
+
expected_dim: int,
|
|
391
|
+
record_index: int,
|
|
392
|
+
errors: list[str],
|
|
393
|
+
) -> None:
|
|
394
|
+
"""Validate the vector field (optional).
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
record: Record to validate.
|
|
398
|
+
expected_dim: Expected vector dimensions.
|
|
399
|
+
record_index: Index for error messages.
|
|
400
|
+
errors: List to append errors to.
|
|
401
|
+
"""
|
|
402
|
+
vector = record.get("vector")
|
|
403
|
+
|
|
404
|
+
# Vector is optional (will be generated during import)
|
|
405
|
+
if vector is None:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
# Check type is list or tuple
|
|
409
|
+
if not isinstance(vector, (list, tuple)):
|
|
410
|
+
errors.append(
|
|
411
|
+
f"Record {record_index}: 'vector' must be a list, "
|
|
412
|
+
f"got {type(vector).__name__}"
|
|
413
|
+
)
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
# Check dimensions
|
|
417
|
+
actual_dim = len(vector)
|
|
418
|
+
if actual_dim != expected_dim:
|
|
419
|
+
errors.append(
|
|
420
|
+
f"Record {record_index}: vector dimension mismatch - "
|
|
421
|
+
f"expected {expected_dim}, got {actual_dim}"
|
|
422
|
+
)
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
# Check all values are numeric
|
|
426
|
+
for i, val in enumerate(vector):
|
|
427
|
+
if not isinstance(val, (int, float)):
|
|
428
|
+
errors.append(
|
|
429
|
+
f"Record {record_index}: 'vector[{i}]' must be numeric, "
|
|
430
|
+
f"got {type(val).__name__}"
|
|
431
|
+
)
|
|
432
|
+
return # One error for vector values is enough
|