Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,131 @@
|
||||
"""M6-07: document the intentional citation-immutability boundary.
|
||||
|
||||
`docs/05_RETRIEVAL/CITATIONS.md` is explicit: a citation is immutable, and
|
||||
resolving one "re-authorizes and returns only content the caller may
|
||||
currently read" -- scoped to workspace/space membership. It does not say
|
||||
document/version lifecycle state revokes a citation, and
|
||||
`test_citations.py::test_citation_survives_document_supersession_and_creation_is_idempotent`
|
||||
already proves supersession doesn't either. `CitationService.resolve()`
|
||||
and `PostgresCitationRepository.get_authorized()` both confirm this by
|
||||
construction: neither joins against `documents`/`document_versions` at
|
||||
all, only `citations.workspace_id`/`citations.space_id`.
|
||||
|
||||
This test locks that boundary in explicitly for M6-07 (rather than leaving
|
||||
it merely implied by what other tests happen not to check), and proves the
|
||||
control that *does* apply -- deactivating the knowledge space the citation
|
||||
belongs to -- correctly revokes access, since `effective_space_ids` is
|
||||
recomputed fresh on every request from the space's current active state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.application.citations import CitationService
|
||||
from ragcore.application.retrieval.authorization import AuthorizedRetrievalScope
|
||||
from ragcore.domain.authorization import Principal, PrincipalType, ResourceNotFound
|
||||
from ragcore.domain.citations import Citation
|
||||
from ragcore.domain.control import Role
|
||||
|
||||
|
||||
class InMemoryCitationRepository:
|
||||
"""Mirrors PostgresCitationRepository.get_authorized exactly: workspace
|
||||
and space membership only, no join against document/version state."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.rows: dict[object, Citation] = {}
|
||||
|
||||
async def create(self, value: Citation) -> Citation:
|
||||
self.rows.setdefault(value.id, value)
|
||||
return self.rows[value.id]
|
||||
|
||||
async def get_authorized(
|
||||
self, citation_id: UUID, *, workspace_id: UUID, effective_space_ids: frozenset[UUID]
|
||||
) -> Citation | None:
|
||||
value = self.rows.get(citation_id)
|
||||
if (
|
||||
value is None
|
||||
or value.workspace_id != workspace_id
|
||||
or value.space_id not in effective_space_ids
|
||||
):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _citation(*, workspace_id: UUID, space_id: UUID, document_id: UUID) -> Citation:
|
||||
return Citation.create(
|
||||
workspace_id=workspace_id,
|
||||
space_id=space_id,
|
||||
document_id=document_id,
|
||||
document_version_id=uuid4(),
|
||||
chunk_id=uuid4(),
|
||||
parent_chunk_id=None,
|
||||
location={"page": 1},
|
||||
excerpt="A cited excerpt from a document that will later be deleted.",
|
||||
source_uri="https://example.test/doc.md",
|
||||
acquired_at=datetime(2026, 8, 1, 12, 0, tzinfo=UTC),
|
||||
retrieval_run_id=uuid4(),
|
||||
index_alias="rag_dense_hybrid_active",
|
||||
profile_version="hybrid-v1",
|
||||
)
|
||||
|
||||
|
||||
def _principal(workspace_id: UUID) -> Principal:
|
||||
return Principal(
|
||||
id=uuid4(), workspace_id=workspace_id, principal_type=PrincipalType.HUMAN,
|
||||
roles=frozenset({Role.VIEWER}),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.asyncio
|
||||
async def test_citation_to_a_deleted_document_remains_resolvable_by_deliberate_design() -> None:
|
||||
"""Intentional (see docs/05_RETRIEVAL/CITATIONS.md): a citation is an
|
||||
immutable historical record, re-authorized by workspace/space on every
|
||||
read -- not revoked because the source document was later deleted."""
|
||||
|
||||
workspace_id, space_id, document_id = uuid4(), uuid4(), uuid4()
|
||||
repository = InMemoryCitationRepository()
|
||||
service = CitationService(repository)
|
||||
scope = AuthorizedRetrievalScope(workspace_id, frozenset({space_id}))
|
||||
created = await service.create(
|
||||
_principal(workspace_id), scope, _citation(
|
||||
workspace_id=workspace_id, space_id=space_id, document_id=document_id
|
||||
)
|
||||
)
|
||||
|
||||
# The document is now deleted; the repository has no notion of that at
|
||||
# all (it never stored a document/version reference to check), which is
|
||||
# exactly the point -- the citation's own excerpt is self-contained.
|
||||
resolved = await service.resolve(_principal(workspace_id), scope, created.id)
|
||||
|
||||
assert resolved.id == created.id
|
||||
assert resolved.excerpt == created.excerpt
|
||||
|
||||
|
||||
@pytest.mark.security
|
||||
@pytest.mark.asyncio
|
||||
async def test_citation_becomes_unresolvable_once_its_space_is_no_longer_in_scope() -> None:
|
||||
"""The control that DOES apply: effective_space_ids is recomputed fresh
|
||||
per request from the space's current grant/active state, so deactivating
|
||||
or revoking access to a space immediately revokes its citations too."""
|
||||
|
||||
workspace_id, space_id, document_id = uuid4(), uuid4(), uuid4()
|
||||
repository = InMemoryCitationRepository()
|
||||
service = CitationService(repository)
|
||||
scope_with_access = AuthorizedRetrievalScope(workspace_id, frozenset({space_id}))
|
||||
created = await service.create(
|
||||
_principal(workspace_id), scope_with_access, _citation(
|
||||
workspace_id=workspace_id, space_id=space_id, document_id=document_id
|
||||
)
|
||||
)
|
||||
|
||||
# A later request where the space is no longer in the caller's
|
||||
# effective scope (deactivated, or the grant was revoked).
|
||||
scope_without_access = AuthorizedRetrievalScope(workspace_id, frozenset())
|
||||
|
||||
with pytest.raises(ResourceNotFound):
|
||||
await service.resolve(_principal(workspace_id), scope_without_access, created.id)
|
||||
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.application.citations import CitationService
|
||||
from ragcore.application.retrieval.authorization import AuthorizedRetrievalScope
|
||||
from ragcore.domain.authorization import Principal, PrincipalType, ResourceNotFound
|
||||
from ragcore.domain.citations import Citation
|
||||
from ragcore.domain.control import Role
|
||||
|
||||
|
||||
def citation(*, workspace_id, space_id, version_id=None) -> Citation:
|
||||
return Citation.create(
|
||||
workspace_id=workspace_id,
|
||||
space_id=space_id,
|
||||
document_id=uuid4(),
|
||||
document_version_id=version_id or uuid4(),
|
||||
chunk_id=uuid4(),
|
||||
parent_chunk_id=None,
|
||||
location={"page": 3, "line_start": 10, "line_end": 14},
|
||||
excerpt="A stable cited excerpt.",
|
||||
source_uri="https://example.test/runbook.md",
|
||||
acquired_at=datetime(2026, 8, 1, 12, 0, tzinfo=UTC),
|
||||
retrieval_run_id=uuid4(),
|
||||
index_alias="rag_dense_hybrid_active",
|
||||
profile_version="hybrid-v1",
|
||||
)
|
||||
|
||||
|
||||
class InMemoryCitationRepository:
|
||||
def __init__(self) -> None:
|
||||
self.rows: dict[object, Citation] = {}
|
||||
|
||||
async def create(self, value: Citation) -> Citation:
|
||||
self.rows.setdefault(value.id, value)
|
||||
return self.rows[value.id]
|
||||
|
||||
async def get_authorized(
|
||||
self,
|
||||
citation_id,
|
||||
*,
|
||||
workspace_id,
|
||||
effective_space_ids,
|
||||
) -> Citation | None:
|
||||
value = self.rows.get(citation_id)
|
||||
if (
|
||||
value is None
|
||||
or value.workspace_id != workspace_id
|
||||
or value.space_id not in effective_space_ids
|
||||
):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def principal(workspace_id) -> Principal:
|
||||
return Principal(
|
||||
id=uuid4(),
|
||||
workspace_id=workspace_id,
|
||||
principal_type=PrincipalType.HUMAN,
|
||||
roles=frozenset({Role.VIEWER}),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_citation_survives_document_supersession_and_creation_is_idempotent() -> None:
|
||||
workspace_id = uuid4()
|
||||
space_id = uuid4()
|
||||
scope = AuthorizedRetrievalScope(workspace_id, frozenset({space_id}))
|
||||
repository = InMemoryCitationRepository()
|
||||
service = CitationService(repository)
|
||||
first = citation(workspace_id=workspace_id, space_id=space_id)
|
||||
|
||||
stored_first = await service.create(principal(workspace_id), scope, first)
|
||||
stored_again = await service.create(principal(workspace_id), scope, first)
|
||||
superseding = citation(
|
||||
workspace_id=workspace_id,
|
||||
space_id=space_id,
|
||||
version_id=uuid4(),
|
||||
)
|
||||
await service.create(principal(workspace_id), scope, superseding)
|
||||
|
||||
resolved = await service.resolve(principal(workspace_id), scope, first.id)
|
||||
|
||||
assert stored_first == stored_again == first
|
||||
assert resolved == first
|
||||
assert len(repository.rows) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_guessed_or_forbidden_citation_ids_are_hidden() -> None:
|
||||
workspace_id = uuid4()
|
||||
allowed_space = uuid4()
|
||||
forbidden_space = uuid4()
|
||||
scope = AuthorizedRetrievalScope(workspace_id, frozenset({allowed_space}))
|
||||
repository = InMemoryCitationRepository()
|
||||
service = CitationService(repository)
|
||||
allowed_principal = principal(workspace_id)
|
||||
forbidden = citation(workspace_id=workspace_id, space_id=forbidden_space)
|
||||
repository.rows[forbidden.id] = forbidden
|
||||
|
||||
with pytest.raises(ResourceNotFound):
|
||||
await service.resolve(allowed_principal, scope, forbidden.id)
|
||||
with pytest.raises(ResourceNotFound):
|
||||
await service.resolve(allowed_principal, scope, uuid4())
|
||||
with pytest.raises(ResourceNotFound):
|
||||
await service.create(allowed_principal, scope, forbidden)
|
||||
|
||||
|
||||
def test_excerpt_checksum_and_location_are_stable_and_tampering_fails() -> None:
|
||||
workspace_id = uuid4()
|
||||
space_id = uuid4()
|
||||
inputs = {
|
||||
"workspace_id": workspace_id,
|
||||
"space_id": space_id,
|
||||
"document_id": uuid4(),
|
||||
"document_version_id": uuid4(),
|
||||
"chunk_id": uuid4(),
|
||||
"parent_chunk_id": None,
|
||||
"location": {"line_end": 14, "line_start": 10, "page": 3},
|
||||
"excerpt": "A stable cited excerpt.",
|
||||
"source_uri": "https://example.test/runbook.md",
|
||||
"acquired_at": datetime(2026, 8, 1, 12, 0, tzinfo=UTC),
|
||||
"retrieval_run_id": uuid4(),
|
||||
"index_alias": "rag_dense_hybrid_active",
|
||||
"profile_version": "hybrid-v1",
|
||||
}
|
||||
first = Citation.create(**inputs)
|
||||
second = Citation.create(**inputs)
|
||||
|
||||
assert first.id == second.id
|
||||
assert first.excerpt_checksum == second.excerpt_checksum
|
||||
assert list(first.location) == ["line_end", "line_start", "page"]
|
||||
|
||||
with pytest.raises(ValueError, match="checksum"):
|
||||
Citation(
|
||||
**{
|
||||
**inputs,
|
||||
"id": first.id,
|
||||
"excerpt_checksum": "0" * 64,
|
||||
}
|
||||
)
|
||||
def test_location_and_identity_cannot_be_forged_after_creation() -> None:
|
||||
workspace_id = uuid4()
|
||||
space_id = uuid4()
|
||||
value = citation(workspace_id=workspace_id, space_id=space_id)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
value.location["page"] = 99 # type: ignore[index]
|
||||
|
||||
with pytest.raises(ValueError, match="id does not match"):
|
||||
Citation(
|
||||
**{
|
||||
"id": uuid4(),
|
||||
"workspace_id": value.workspace_id,
|
||||
"space_id": value.space_id,
|
||||
"document_id": value.document_id,
|
||||
"document_version_id": value.document_version_id,
|
||||
"chunk_id": value.chunk_id,
|
||||
"parent_chunk_id": value.parent_chunk_id,
|
||||
"location": dict(value.location),
|
||||
"excerpt": value.excerpt,
|
||||
"excerpt_checksum": value.excerpt_checksum,
|
||||
"source_uri": value.source_uri,
|
||||
"acquired_at": value.acquired_at,
|
||||
"retrieval_run_id": value.retrieval_run_id,
|
||||
"index_alias": value.index_alias,
|
||||
"profile_version": value.profile_version,
|
||||
}
|
||||
)
|
||||
Reference in new issue
Block a user