Prefix-isolation primitives shared across all memory backends (v8.0 §5).
The isolation rule (design §5.d): a query at path P sees a memory at path Q
iff Q is a prefix of P. This is enforced via exact-set-membership over
the query's ancestor-prefix strings — NEVER a LIKE 'prefix%' scan, which
would leak acme-corp into an acme query (the §5.b R2 CRITICAL class).
Filtering is ALWAYS-ON (it closes the Item D cross-session bleed bug) and
is NOT gated behind scorer_on_hot_path. Weighted blending (§4) is the
opt-in part; isolation is unconditional.
The materialised scope_path is stored in the zero-migration property bag
(MemoryNode.properties['scope_path'] / vector metadata scope_path) so
no schema change is required. Pre-v8.0 rows have no scope_path; they
dual-read as a 1-level root path derived from tenant_id (design §6.b),
keeping them tenant-global as before.
is_visible
is_visible(
stored_path: str, query_prefixes: list[str]
) -> bool
Return True iff stored_path is one of the query's ancestor prefixes.
Exact set membership — the §5.d isolation invariant. query_prefixes is
:meth:TenantScope.ancestor_prefix_paths. NEVER a startswith.
Source code in src/symfonic/memory/scope_isolation.py
| def is_visible(
stored_path: str,
query_prefixes: list[str],
) -> bool:
"""Return True iff ``stored_path`` is one of the query's ancestor prefixes.
Exact set membership — the §5.d isolation invariant. ``query_prefixes`` is
:meth:`TenantScope.ancestor_prefix_paths`. NEVER a ``startswith``.
"""
return stored_path in query_prefixes
|
materialise_scope_path
materialise_scope_path(scope: TenantScope) -> str
Return the materialised scope_path string for a write scope.
Source code in src/symfonic/memory/scope_isolation.py
| def materialise_scope_path(scope: TenantScope) -> str:
"""Return the materialised ``scope_path`` string for a write scope."""
return scope.scope_path
|
metadata_scope_path(
metadata: dict[str, object] | None, tenant_id: str
) -> str
Vector-backend variant of :func:stored_scope_path (metadata bag).
Source code in src/symfonic/memory/scope_isolation.py
| def metadata_scope_path(
metadata: dict[str, object] | None,
tenant_id: str,
) -> str:
"""Vector-backend variant of :func:`stored_scope_path` (metadata bag)."""
return stored_scope_path(metadata, tenant_id)
|
mongo_prefix_filter
mongo_prefix_filter(
scope: TenantScope, field: str = SCOPE_PATH_KEY
) -> dict
Build the Mongo prefix-isolation filter for a scope_path field.
Returns a query fragment matching docs whose field is one of the
query's ancestor prefixes (exact $in — NEVER a $regex prefix,
which would re-introduce the false-prefix-leak class). Includes the §6.b
dual-read for pre-v8.0 docs missing the field: they behave as a 1-level
root path. Compose into a larger find filter with $or-free AND by
merging into the top-level dict, or under $and when other keys overlap.
Source code in src/symfonic/memory/scope_isolation.py
| def mongo_prefix_filter(scope: TenantScope, field: str = SCOPE_PATH_KEY) -> dict:
"""Build the Mongo prefix-isolation filter for a scope_path field.
Returns a query fragment matching docs whose ``field`` is one of the
query's ancestor prefixes (exact ``$in`` — NEVER a ``$regex`` prefix,
which would re-introduce the false-prefix-leak class). Includes the §6.b
dual-read for pre-v8.0 docs missing the field: they behave as a 1-level
root path. Compose into a larger ``find`` filter with ``$or``-free AND by
merging into the top-level dict, or under ``$and`` when other keys overlap.
"""
prefixes = scope.ancestor_prefix_paths()
legacy = legacy_scope_path(scope.tenant_id)
cond: dict = {field: {"$in": prefixes}}
if legacy in prefixes:
# Legacy (missing/absent) docs read as the root path; include them only
# when the root path is on the query's ancestor chain.
return {"$or": [cond, {field: {"$exists": False}}, {field: None}]}
return cond
|
sql_prefix_condition
sql_prefix_condition(
scope: TenantScope, column: str, next_param_index: int
) -> tuple[str, list[object]]
Build the SQL prefix-isolation predicate for a materialised column.
Returns (sql_fragment, params) where the fragment is an exact-IN over
the query's ancestor prefixes (NEVER LIKE — the §5.b R2 guard):
(col = ANY($N::text[]) OR (col IS NULL AND $M = ANY($N::text[])))
The first branch matches v8.0 rows; the second is the §6.b dual-read for
pre-v8.0 rows whose scope_path is NULL — they behave as a 1-level root
path tenant\x1f<tenant_id>, visible to any descendant query under that
tenant. next_param_index is the 1-based positional placeholder to use
first (asyncpg $N style); the function consumes two placeholders.
Source code in src/symfonic/memory/scope_isolation.py
| def sql_prefix_condition(
scope: TenantScope,
column: str,
next_param_index: int,
) -> tuple[str, list[object]]:
"""Build the SQL prefix-isolation predicate for a materialised column.
Returns ``(sql_fragment, params)`` where the fragment is an exact-IN over
the query's ancestor prefixes (NEVER ``LIKE`` — the §5.b R2 guard):
(col = ANY($N::text[]) OR (col IS NULL AND $M = ANY($N::text[])))
The first branch matches v8.0 rows; the second is the §6.b dual-read for
pre-v8.0 rows whose ``scope_path`` is NULL — they behave as a 1-level root
path ``tenant\\x1f<tenant_id>``, visible to any descendant query under that
tenant. ``next_param_index`` is the 1-based positional placeholder to use
first (asyncpg ``$N`` style); the function consumes two placeholders.
"""
prefixes = scope.ancestor_prefix_paths()
legacy = legacy_scope_path(scope.tenant_id)
arr_idx = next_param_index
legacy_idx = next_param_index + 1
fragment = (
f"({column} = ANY(${arr_idx}::text[]) "
f"OR ({column} IS NULL AND ${legacy_idx} = ANY(${arr_idx}::text[])))"
)
return fragment, [prefixes, legacy]
|
stored_scope_path
stored_scope_path(
properties: dict[str, object] | None, tenant_id: str
) -> str
Resolve a stored row's effective scope_path (with dual-read).
Reads properties['scope_path'] when present. Pre-v8.0 rows (no key)
fall back to the 1-level root path tenant\x1f<tenant_id> (design §6.b)
— visible to every descendant query under that tenant, the correct
conservative default.
Source code in src/symfonic/memory/scope_isolation.py
| def stored_scope_path(
properties: dict[str, object] | None,
tenant_id: str,
) -> str:
"""Resolve a stored row's effective ``scope_path`` (with dual-read).
Reads ``properties['scope_path']`` when present. Pre-v8.0 rows (no key)
fall back to the 1-level root path ``tenant\\x1f<tenant_id>`` (design §6.b)
— visible to every descendant query under that tenant, the correct
conservative default.
"""
raw = (properties or {}).get(SCOPE_PATH_KEY)
if isinstance(raw, str) and raw:
return raw
return legacy_scope_path(tenant_id)
|