feat(provider): validate scope_filter against the v0.5 4-axis lattice

bifrost 0.7.0 (wire v0.5) makes agent_self canonical: the scope lattice is
now {end_user, group, tenant, agent_self}. Our store was MORE permissive than
bifrost's reference (no _validate_scope_filter), which silently 0-zeroed the
#295 cold recall instead of a loud 400. Now matched: search rejects an
out-of-lattice axis with InvalidFilter (-> memory.invalid_filter 400), agent_self
admitted. Purely additive — everything that validated before still validates.

Closes the parity gap our own foot-gun flag opened (bifrost-dev shipped the
lattice add #10 off it). Pin bumped bifrost>=0.6.1 -> >=0.7.0. Contract
search PRE-003 + lattice_axes test; 2 new store tests; full suite 429 green.
This commit is contained in:
vh
2026-06-16 01:24:01 -07:00
parent ca02c70b7c
commit aac4353933
5 changed files with 46 additions and 9 deletions
+28 -1
View File
@@ -10,7 +10,12 @@ from __future__ import annotations
import types
import pytest
from bifrost.memory import IdempotencyConflict, InvalidArguments, RevisionMismatch
from bifrost.memory import (
IdempotencyConflict,
InvalidArguments,
InvalidFilter,
RevisionMismatch,
)
from ratatoskr.provider.memory_store import (
build_memory_provider_app,
@@ -221,6 +226,28 @@ async def test_search_non_dict_scope_filter_rejected():
await store.search(_vec(1.0), top_k=5, scope_filter="u1")
async def test_search_out_of_lattice_scope_axis_rejected():
# v0.5 scope lattice = {end_user, group, tenant, agent_self}; an axis outside
# it is InvalidFilter (-> memory.invalid_filter 400), matching bifrost's reference.
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
with pytest.raises(InvalidFilter):
await store.search(_vec(1.0), top_k=5, scope_filter={"bogus_axis": "x"})
async def test_search_agent_self_axis_accepted():
# agent_self became canonical at wire v0.5 (#10) — admitted, not rejected.
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)
await store.upsert_many(
[_chunk("a1", scope={"agent_self": "ratatoskr:smoke"})],
idempotency_key="k1",
ctx=_ctx(),
)
results = await store.search(
_vec(1.0), top_k=5, scope_filter={"agent_self": "ratatoskr:smoke"}
)
assert [r["chunk_id"] for r in results] == ["a1"]
async def test_search_top_k_zero_returns_empty():
# POST-001: at most top_k — zero means zero
store = open_memory_store(":memory:", embedding_dim=EMBEDDING_DIM)