Lessons Learned — Neural Memory¶
Recurring mistakes and patterns to avoid. Updated after each audit cycle.
2026-02-24 — v2.8.1 Audit¶
1. Localhost Restriction Must Be Router-Level, Not Per-Endpoint¶
What happened: memory_router, brain_router, dashboard_router had zero localhost
protection. Only hub, oauth, and openclaw routers applied it — and each had their
own duplicate _require_local_request function.
Root cause: No shared dependency. Each developer copy-pasted the guard into their own route file, so new routers forgot to add it.
Fix: Centralized require_local_request in server/dependencies.py. Applied at
router level via dependencies=[Depends(require_local_request)].
Rule: Every new APIRouter in server/routes/ MUST include
dependencies=[Depends(require_local_request)] unless explicitly designed for public access.
Add this to the PR checklist.
2. Brain Name Validation Must Be Consistent Across Layers¶
What happened: unified_config.py validated brain names with ^[a-zA-Z0-9_\-\.]+$
AND is_relative_to(), but the REST CreateBrainRequest model only checked max_length=100.
A malicious name like ../../etc/passwd could reach the config layer.
Root cause: Defense-in-depth was incomplete. The Pydantic model was the first validation boundary but was the weakest.
Fix: Added pattern=r"^[a-zA-Z0-9_\-\.]+$" to CreateBrainRequest.name Field.
Rule: Input validation at the API boundary must be at least as strict as downstream validation. Never rely solely on internal code to reject bad input — fail fast at the edge.
3. File Path Inputs Need is_relative_to() Guard¶
What happened: db_train_handler.py accepted a SQLite connection string, resolved it
with Path.resolve(), checked is_file() — but never checked if the resolved path was
within the working directory. An MCP client could read arbitrary .db files on the system.
Root cause: Path traversal prevention was applied in unified_config.py for brain
DB paths but not in the newer db_train_handler.py.
Fix: Added is_relative_to(Path.cwd().resolve()) check before is_file().
Rule: Any user-provided file path MUST be validated with:
This applies to: connection strings, import paths, training paths, export paths.4. Test Mocks Must Match Actual Code Flow¶
What happened: test_transplant_nonexistent_source mocked find_brain_by_name on
the storage object, but _transplant handler never calls find_brain_by_name — it calls
get_shared_storage(brain_name=...). The mock did nothing, and the test hit real code
paths with AsyncMock objects cascading into merge.py, causing TypeError.
Root cause: Test was written against an assumed API contract, not the actual implementation. The handler was refactored but the test wasn't updated.
Fix: Patched neural_memory.unified_config.get_shared_storage instead.
Rule: When writing test mocks, trace the actual code path to verify which functions are called. Don't assume — read the handler code. After refactoring a handler, always update its tests.
5. _require_brain_id Callers Must Handle ValueError¶
What happened: _recall handler called _require_brain_id(storage) without
try/except. When _current_brain_id was None, it raised ValueError instead of
returning {"error": "No brain configured"}.
Root cause: 7 handlers used the same _require_brain_id → get_brain pattern but
only some had error handling. The helper raises ValueError by design, but callers
were inconsistent about catching it.
Fix: Added try/except ValueError around _require_brain_id in _recall.
Rule: Every call to _require_brain_id() MUST be wrapped in try/except ValueError,
or the handler must be structured to guarantee a brain context exists before calling it.
Consider refactoring to a helper that returns dict | str (error dict or brain_id).
6. Optional Dependencies Need Graceful Test Skipping¶
What happened: tests/test_encryption.py imported MemoryEncryptor which imports
cryptography at runtime. CI installs [dev,server] but not [encryption], so all
encryption tests failed with ModuleNotFoundError.
Root cause: Tests didn't account for optional extras not being installed in CI.
Fix: Added pytest.mark.skipif(not _HAS_CRYPTOGRAPHY, ...) on the test class.
Rule: Tests for optional-extra features MUST use pytest.importorskip() or
@pytest.mark.skipif to gracefully skip when the dependency is missing. CI matrix
should document which extras are installed.
7. Always Run Full CI Checks Locally Before Pushing¶
What happened: After fixing tests, pushed 3 separate commits because lint, format, and mypy errors were discovered incrementally on CI.
Root cause: Only ran pytest locally, not ruff check, ruff format --check,
or mypy.
Rule: Before pushing, always run the full CI pipeline locally:
ruff check src/ tests/
ruff format --check src/ tests/
mypy src/ --ignore-missing-imports
pytest tests/ -x --timeout=60
/ship skill that runs all checks.
Checklist for New Features¶
- All new
APIRouterinstances havedependencies=[Depends(require_local_request)] - All user-provided paths validated with
is_relative_to() - Brain names validated with
^[a-zA-Z0-9_\-\.]+$at API boundary - Tests for optional-extra features skip gracefully
- Test mocks verified against actual code flow (not assumed)
-
_require_brain_id()calls wrapped in try/except ValueError - Full lint + format + mypy + test run before push