fix(storage): route the last two id sanitisers through the single source + scope every record-id fetch to the brain#63
Conversation
…rce + scope every record-id fetch to the brain
Follow-ups to the _to_surreal_id consolidation.
Single-source sanitisation:
* migrations._sanitize_id (dash-only .replace) and an inline .replace("-","_")
in tool_events were the two id sanitisers that still bypassed the _ids.py
choke point. Both now call _to_surreal_id. migrations gains the full
[A-Za-z0-9_] fold instead of a dash-only replace, which also fixes a latent
mismatch: neuron records are stored folded, so a legacy endpoint id with a '.'
used to migrate to a non-matching neuron:{id} edge. For uuid4 / hash ids the
output is unchanged.
Brain-scoped record fetches:
* Every fetch-by-record-id did a bare select with no brain filter, so a caller
could read another brain's record just by knowing its id. All of them now add
WHERE brain_id = $brain_id (bound param; brain_id from _get_brain_id ->
_safe_brain_id, fail-closed): get_neuron, get_neuron_state, get_synapse,
get_fiber, get_neurons_batch, find_neurons_by_ids. They stay direct record
fetches (ids pinned in FROM, not a table scan), so the dashboard-perf property
is preserved; the WHERE only rejects a cross-brain hit. (device fetch was
already brain-keyed in its record id.)
Tests: brain-scope query shape + bound param for all six methods; the
fail-closed brain-id path; single-source routing for migrations (dotted-id fold)
and tool_events. Full unit suite green (-n0).
| 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", |
There was a problem hiding this comment.
You flagged the two batch methods; the same bare-select read-by-id class was also open in get_neuron, get_neuron_state, get_synapse and get_fiber, so I scoped all of them here rather than leave a half-guarantee — a caller could otherwise still read a foreign brain's record via the single-record path. brain_id is a bound param from _get_brain_id() (→ _safe_brain_id, fail-closed), and the record stays pinned in FROM, so it's still a direct fetch, not a scan. Routing through _query also gives the single getters the reconnect-on-dropped-transport retry they lacked. (device was already brain-keyed in its record id.)
| 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", |
There was a problem hiding this comment.
The ids in FROM already pass _to_surreal_id + an isalnum()/_ filter, so the WHERE only adds the brain check on records already pinned by id — no change to the 2.7.2 dashboard-perf profile (still a pinned-record fetch, not a FROM neuron scan).
| sid = _record_part(row["id"]) | ||
| src = _sanitize_id(str(row.get("source_id", ""))) | ||
| tgt = _sanitize_id(str(row.get("target_id", ""))) | ||
| src = _to_surreal_id(str(row.get("source_id", ""))) |
There was a problem hiding this comment.
Second follow-up: the old _sanitize_id was dash-only, so it both duplicated the fold and under-folded. _to_surreal_id here also quietly corrects endpoints — neuron records are stored folded, so a legacy source_id/target_id with a . (or any non-- char) previously produced a neuron:{id} endpoint that didn't match the stored record. uuid4 / hash ids are unaffected.
Hi Toni!
The two follow-ups you tracked on the id-sanitiser consolidation — thank you again for that review. Both are small and neither is reachable with today's inputs, but they each nick a guarantee, so here they are closed together.
1. Two sanitisers still bypassed the single choke point
_ids._to_surreal_idis meant to be the only place a caller-supplied id is folded to[A-Za-z0-9_]. Two copies still lived outside it:migrations._sanitize_id— a dash-only.replace("-", "_"). Replaced with_to_surreal_id. Besides restoring the single-source guarantee, this fixes a latent mismatch: neuron records are stored folded (_to_surreal_id), so a legacy v7 endpoint id containing anything other than-(e.g. a.) used to migrate to aneuron:{id}edge endpoint that did not match the stored neuron. For uuid4 / content-hash ids (the only ids in practice) the output is unchanged.tool_events.insert_tool_events— an inlineevent_id.replace("-", "_")on a server-generated uuid4. Routed through_to_surreal_id; output is identical for a uuid4, but the id no longer has a private folding path.2. Record-id fetches were not brain-scoped
Every fetch-by-record-id did a bare
select(neuron:{sid},synapse:{sid}, …) with noWHERE brain_id, so a caller in brain A could read a record owned by brain B just by knowing (or guessing) its id — the same gap the store-layer guard closed forconsolidation.py. You flaggedget_neurons_batch/find_neurons_by_ids; the same class was open inget_neuron,get_neuron_state,get_synapseandget_fiber, so I closed the whole class here rather than leave a half-guarantee. All six now addWHERE brain_id = $brain_id(parameterised;brain_idfrom_get_brain_id()→_safe_brain_id, fail-closed). (devicefetch was already brain-keyed in its record id.)They stay direct record fetches — the ids are still pinned in
FROM neuron:{sid}, …, so this is not a table scan and the 2.7.2 dashboard-perf property is preserved; theWHEREonly rejects a cross-brain hit on records already pinned by id. The single-record getters also gain the reconnect-on-dropped-transport retry (they route through_querynow instead of a bareselect).Type of Change
Testing
tests/unit/test_id_consolidation_and_brain_scope.py— brain-scope query shape + bound param for all six methods; fail-closed brain-id path; single-source routing for migrations (dotted-id fold) and tool_events.test_dashboard_perf_queries.py::test_fetches_records_directly_with_omitto pin the scoped-but-still-direct fetch (was assertingWHERE not in sql).main): 6,090 passed, 33 skipped in ~29 s (-n0), clean exit.ruff+mypyclean.surrealdb:v3.2.0: for all six methods the owning brain reads the record and a foreign brain getsNone.Checklist
CHANGELOG.md— happy to add a Security/Fixed line if you'd like.Callers audited (why the scope is a no-op for legitimate use)
All ids reaching these methods are derived from the current brain's own graph/fibers:
get_neuron/get_neuron_state(anchor ids from this brain's fibers), the batch methods (retrieval_context, activation, consolidation, dashboard_api, compression, query_expansion),find_neurons_by_ids(the graph-view route). None cross a brain.Robert