-
Notifications
You must be signed in to change notification settings - Fork 1
fix(storage): route the last two id sanitisers through the single source + scope every record-id fetch to the brain #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -620,29 +620,41 @@ async def add_neuron(self, neuron: Neuron) -> str: | |
| return neuron.id | ||
|
|
||
| async def get_neuron(self, neuron_id: str) -> Neuron | None: | ||
| conn = self._ensure_conn() | ||
| # Scope to the current brain: a bare record select would let a caller | ||
| # read another brain's neuron by id. The record is still pinned in FROM. | ||
| brain_id = self._get_brain_id() | ||
| sid = _to_surreal_id(neuron_id) | ||
| try: | ||
| result = await conn.select(f"neuron:{sid}") | ||
| if result and isinstance(result, list) and len(result) > 0: | ||
| return _row_to_neuron(result[0]) | ||
| rows = await self._query( | ||
| f"SELECT * FROM neuron:{sid} WHERE brain_id = $brain_id", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You flagged the two batch methods; the same bare- |
||
| brain_id=brain_id, | ||
| ) | ||
| if rows: | ||
| return _row_to_neuron(rows[0]) | ||
| except Exception: | ||
| pass | ||
| return None | ||
|
|
||
| async def get_neurons_batch(self, neuron_ids: list[str]) -> dict[str, Neuron]: | ||
| if not neuron_ids: | ||
| return {} | ||
| # Use direct select for each ID (more reliable than IN query with params) | ||
| # Scope every fetch to the current brain. These are direct record-id | ||
| # selects, so without a brain_id filter a caller could read a neuron | ||
| # owned by another brain just by knowing (or guessing) its id. The FROM | ||
| # is still a pinned record (neuron:{sid}), so this stays a direct fetch, | ||
| # not a table scan — the brain_id check only rejects a cross-brain hit. | ||
| brain_id = self._get_brain_id() | ||
| results: dict[str, Neuron] = {} | ||
| for nid in neuron_ids: | ||
| sid = _to_surreal_id(nid) | ||
| try: | ||
| result = await self._conn.select(f"neuron:{sid}") | ||
| if result and isinstance(result, list) and len(result) > 0: | ||
| neuron = _row_to_neuron(result[0]) | ||
| # Use the converted ID as key | ||
| results[nid] = neuron | ||
| rows = await self._query( | ||
| f"SELECT * FROM neuron:{sid} WHERE brain_id = $brain_id", | ||
| brain_id=brain_id, | ||
| ) | ||
| if rows: | ||
| # Use the original ID as key | ||
| results[nid] = _row_to_neuron(rows[0]) | ||
| except Exception: | ||
| pass | ||
| return results | ||
|
|
@@ -718,12 +730,19 @@ async def find_neurons_by_ids( | |
| ] | ||
| if not safe: | ||
| return [] | ||
| # Scope to the current brain: the FROM is a pinned record-id list (still | ||
| # a direct fetch, not a table scan), and the WHERE brain_id filter keeps | ||
| # a caller from reading another brain's neuron by id. | ||
| brain_id = self._get_brain_id() | ||
| projection = "SELECT *" if include_embedding else "SELECT * OMIT embedding_vec" | ||
| out: list[Neuron] = [] | ||
| # Chunk to keep the FROM record-id list a sane query size. | ||
| for i in range(0, len(safe), 1000): | ||
| things = ", ".join(f"neuron:{s}" for s in safe[i : i + 1000]) | ||
| rows = await self._query(f"{projection} FROM {things}") | ||
| rows = await self._query( | ||
| f"{projection} FROM {things} WHERE brain_id = $brain_id", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ids in |
||
| brain_id=brain_id, | ||
| ) | ||
| out.extend(_row_to_neuron(r) for r in rows) | ||
| return out | ||
|
|
||
|
|
@@ -823,12 +842,15 @@ async def has_neuron_by_content_hash(self, content_hash: int) -> bool: | |
| # ================================================================ | ||
|
|
||
| async def get_neuron_state(self, neuron_id: str) -> NeuronState | None: | ||
| conn = self._ensure_conn() | ||
| brain_id = self._get_brain_id() | ||
| sid = _to_surreal_id(neuron_id) | ||
| try: | ||
| result = await conn.select(f"neuron_state:state_{sid}") | ||
| if result: | ||
| return _row_to_neuron_state(result[0] if isinstance(result, list) else result) | ||
| rows = await self._query( | ||
| f"SELECT * FROM neuron_state:state_{sid} WHERE brain_id = $brain_id", | ||
| brain_id=brain_id, | ||
| ) | ||
| if rows: | ||
| return _row_to_neuron_state(rows[0]) | ||
| except Exception: | ||
| pass | ||
| return None | ||
|
|
@@ -887,12 +909,15 @@ async def add_synapse(self, synapse: Synapse) -> str: | |
| return synapse.id | ||
|
|
||
| async def get_synapse(self, synapse_id: str) -> Synapse | None: | ||
| conn = self._ensure_conn() | ||
| brain_id = self._get_brain_id() | ||
| sid = _to_surreal_id(synapse_id) | ||
| try: | ||
| result = await conn.select(f"synapse:{sid}") | ||
| if result: | ||
| return _row_to_synapse(result[0] if isinstance(result, list) else result) | ||
| rows = await self._query( | ||
| f"SELECT * FROM synapse:{sid} WHERE brain_id = $brain_id", | ||
| brain_id=brain_id, | ||
| ) | ||
| if rows: | ||
| return _row_to_synapse(rows[0]) | ||
| except Exception: | ||
| pass | ||
| return None | ||
|
|
@@ -1185,12 +1210,15 @@ async def add_fiber(self, fiber: Fiber) -> str: | |
| return fiber.id | ||
|
|
||
| async def get_fiber(self, fiber_id: str) -> Fiber | None: | ||
| conn = self._ensure_conn() | ||
| brain_id = self._get_brain_id() | ||
| fid = _to_surreal_id(fiber_id) | ||
| try: | ||
| result = await conn.select(f"fiber:{fid}") | ||
| if result: | ||
| return _row_to_fiber(result[0] if isinstance(result, list) else result) | ||
| rows = await self._query( | ||
| f"SELECT * FROM fiber:{fid} WHERE brain_id = $brain_id", | ||
| brain_id=brain_id, | ||
| ) | ||
| if rows: | ||
| return _row_to_fiber(rows[0]) | ||
| except Exception: | ||
| pass | ||
| return None | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| """Single-source id sanitisation + brain-scoped batch fetch. | ||
|
|
||
| Two follow-ups on the id-sanitiser consolidation: | ||
|
|
||
| * two weak sanitisers still bypassed the ``_ids.py`` single choke point — | ||
| ``migrations._sanitize_id`` (dash-only ``.replace``) and an inline | ||
| ``.replace("-", "_")`` in ``tool_events`` — so both now route through | ||
| ``_to_surreal_id``; | ||
| * ``get_neurons_batch`` / ``find_neurons_by_ids`` issued direct record-id | ||
| selects with no ``WHERE brain_id``, so a caller could read another brain's | ||
| neuron just by knowing its id — both now scope to the current brain. | ||
|
|
||
| The surrealdb SDK is stubbed (repo convention); the connection is an AsyncMock. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
| from unittest.mock import AsyncMock, MagicMock | ||
|
|
||
| if "surrealdb" not in sys.modules: | ||
| sys.modules["surrealdb"] = MagicMock() | ||
| sys.modules["surrealdb.errors"] = MagicMock() | ||
|
|
||
| import surreal_memory.storage.surrealdb.tool_events as tool_events_mod | ||
| from surreal_memory.storage.surrealdb import migrations | ||
| from surreal_memory.storage.surrealdb._ids import _to_surreal_id | ||
| from surreal_memory.storage.surrealdb.store import SurrealDBStorage | ||
|
|
||
|
|
||
| def _store_with_mock_conn() -> tuple[SurrealDBStorage, AsyncMock]: | ||
| st = SurrealDBStorage(url="http://localhost:8001") | ||
| conn = AsyncMock() | ||
| conn.query = AsyncMock(return_value=[]) | ||
| conn.insert = AsyncMock(return_value=None) | ||
| st._conn = conn | ||
| st._current_brain_id = "b1" | ||
| return st, conn | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- # | ||
| # brain-scoped batch fetch | ||
| # --------------------------------------------------------------------------- # | ||
| class TestBrainScopedBatchFetch: | ||
| async def test_find_neurons_by_ids_scopes_to_current_brain(self): | ||
| st, conn = _store_with_mock_conn() | ||
| await st.find_neurons_by_ids(["abc-123", "def-456"]) | ||
| sql, params = conn.query.call_args.args | ||
| # Still a pinned-record fetch (ids in FROM), never a `FROM neuron` scan… | ||
| assert sql.startswith("SELECT * OMIT embedding_vec FROM neuron:abc_123, neuron:def_456") | ||
| # …but now scoped, so a foreign-brain id cannot be read back. | ||
| assert sql.endswith("WHERE brain_id = $brain_id") | ||
| assert params == {"brain_id": "b1"} | ||
|
|
||
| async def test_get_neurons_batch_scopes_to_current_brain(self): | ||
| st, conn = _store_with_mock_conn() | ||
| await st.get_neurons_batch(["abc-123"]) | ||
| sql, params = conn.query.call_args.args | ||
| assert sql == "SELECT * FROM neuron:abc_123 WHERE brain_id = $brain_id" | ||
| assert params == {"brain_id": "b1"} | ||
|
|
||
| async def test_get_neurons_batch_uses_safe_brain_id(self): | ||
| # An unsafe brain context must fail closed (via _safe_brain_id) rather | ||
| # than inline a hostile id — same guarantee as the rest of the store. | ||
| st, conn = _store_with_mock_conn() | ||
| st._current_brain_id = 'bad"; DELETE neuron' | ||
| import pytest | ||
|
|
||
| with pytest.raises(ValueError): | ||
| await st.get_neurons_batch(["abc-123"]) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- # | ||
| # single-source sanitisation | ||
| # --------------------------------------------------------------------------- # | ||
| class TestSanitizerConsolidation: | ||
| def test_migrations_has_no_local_sanitiser(self): | ||
| # The dash-only duplicate is gone; the single source is _ids._to_surreal_id. | ||
| assert not hasattr(migrations, "_sanitize_id") | ||
|
|
||
| def test_relation_row_folds_via_single_source(self, monkeypatch): | ||
| # A dotted endpoint id must fold '.' -> '_' (full [A-Za-z0-9_] fold), | ||
| # which the removed dash-only sanitiser did NOT do. This both proves the | ||
| # single source is used and fixes a latent bug: neuron records are stored | ||
| # folded, so a dotted legacy id used to migrate to a non-matching edge. | ||
| rid_calls: list[tuple[str, str]] = [] | ||
|
|
||
| def _fake_rid(table: str, ident: object) -> str: | ||
| rid_calls.append((table, str(ident))) | ||
| return f"{table}:{ident}" | ||
|
|
||
| monkeypatch.setattr(sys.modules["surrealdb"], "RecordID", _fake_rid) | ||
| migrations._to_relation_row( | ||
| { | ||
| "id": "synapse:s1", | ||
| "source_id": "a.b-c", | ||
| "target_id": "x-y", | ||
| "brain_id": "default", | ||
| "type": "assoc", | ||
| "weight": 1.0, | ||
| } | ||
| ) | ||
| assert ("neuron", _to_surreal_id("a.b-c")) in rid_calls # -> a_b_c | ||
| assert ("neuron", "a_b_c") in rid_calls | ||
| assert ("neuron", "x_y") in rid_calls | ||
|
|
||
| async def test_tool_events_id_routes_through_single_source(self, monkeypatch): | ||
| st, conn = _store_with_mock_conn() | ||
| monkeypatch.setattr(tool_events_mod, "_to_surreal_id", lambda s: "SENTINEL") | ||
| await st.insert_tool_events( | ||
| "b1", [{"tool_name": "t", "created_at": "2026-01-01T00:00:00+00:00"}] | ||
| ) | ||
| table, doc = conn.insert.call_args.args | ||
| assert table == "tool_events" | ||
| # The record id came from the single-source folder, not a raw .replace. | ||
| assert doc["id"] == "SENTINEL" | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- # | ||
| # single-record fetches close the same read-by-id class | ||
| # --------------------------------------------------------------------------- # | ||
| class TestSingleRecordBrainScope: | ||
| async def test_get_neuron_scopes_to_brain(self): | ||
| st, conn = _store_with_mock_conn() | ||
| await st.get_neuron("abc-123") | ||
| sql, params = conn.query.call_args.args | ||
| assert sql == "SELECT * FROM neuron:abc_123 WHERE brain_id = $brain_id" | ||
| assert params == {"brain_id": "b1"} | ||
|
|
||
| async def test_get_neuron_state_scopes_to_brain(self): | ||
| st, conn = _store_with_mock_conn() | ||
| await st.get_neuron_state("abc-123") | ||
| sql, params = conn.query.call_args.args | ||
| assert sql == "SELECT * FROM neuron_state:state_abc_123 WHERE brain_id = $brain_id" | ||
| assert params == {"brain_id": "b1"} | ||
|
|
||
| async def test_get_synapse_scopes_to_brain(self): | ||
| st, conn = _store_with_mock_conn() | ||
| await st.get_synapse("s-1") | ||
| sql, params = conn.query.call_args.args | ||
| assert sql == "SELECT * FROM synapse:s_1 WHERE brain_id = $brain_id" | ||
| assert params == {"brain_id": "b1"} | ||
|
|
||
| async def test_get_fiber_scopes_to_brain(self): | ||
| st, conn = _store_with_mock_conn() | ||
| await st.get_fiber("f-1") | ||
| sql, params = conn.query.call_args.args | ||
| assert sql == "SELECT * FROM fiber:f_1 WHERE brain_id = $brain_id" | ||
| assert params == {"brain_id": "b1"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second follow-up: the old
_sanitize_idwas dash-only, so it both duplicated the fold and under-folded._to_surreal_idhere also quietly corrects endpoints — neuron records are stored folded, so a legacysource_id/target_idwith a.(or any non--char) previously produced aneuron:{id}endpoint that didn't match the stored record. uuid4 / hash ids are unaffected.