# Scoped Backend View The Scoped Backend View layer provides project-resource isolation for the Advanced Context Management System (ACMS). It ensures that actors and strategies never see resources outside their plan's scope, enforcing cross-project isolation at both the backend query and fragment visibility levels. ## Architecture ``` Plan ──► ResourceScope ──► ScopedBackendView ──► Backend Protocol ──► Physical Store │ │ │ ├── search_text(backend, query) │ ├── search_vector(backend, embedding) │ ├── search_graph(backend, query) │ └── is_visible(fragment) → bool │ ├── resource_ids: frozenset[str] ├── project_names: frozenset[str] ├── include_paths / exclude_paths └── include_resources / exclude_resources ``` Each plan is assigned a `ResourceScope` at creation time. The scope is immutable (frozen) — once created, a plan's visible resources never change. `ScopedBackendView` wraps backend queries to automatically inject the `scope` parameter, and `ScopedBackendSet` bundles scoped views for all three backend types (text, vector, graph). ## ResourceScope Resolved set of resources and projects visible to a plan. Created by `resolve_resource_scope()` during plan creation. Based on `docs/specification.md` lines 42510--42525. | Field | Type | Description | |-------|------|-------------| | `resource_ids` | `frozenset[str]` | Resource ULIDs in scope (empty = no resources after filtering) | | `project_names` | `frozenset[str]` | Namespaced project names in scope (min 1) | | `temporal_scope` | `str` | One of `"current"`, `"recent"`, `"all"` (default: `"current"`) | | `include_resources` | `tuple[str, ...]` | Resource allowlist (names/aliases). Empty = all | | `exclude_resources` | `tuple[str, ...]` | Resource denylist. Applied after allowlist | | `include_paths` | `tuple[str, ...]` | Path glob allowlist. Empty = all | | `exclude_paths` | `tuple[str, ...]` | Path glob denylist. Applied after allowlist | ### Methods | Method | Signature | Returns | |--------|-----------|---------| | `contains_resource` | `(resource_id: str)` | `bool` | | `contains_project` | `(project_name: str)` | `bool` | | `matches_path` | `(path: str)` | `bool` — applies include/exclude glob filters | Path matching uses `pathlib.PurePath.full_match()` which supports recursive `**` glob patterns (e.g., `src/**/*.py`). ## ScopedBackendView Wraps a backend to restrict queries to a resource scope. Operates at two levels: backend-level (injects `scope` into queries) and fragment-level (filters `TieredFragment` by project name and resource ID). Based on `docs/specification.md` lines 42548--42566. | Field | Type | Description | |-------|------|-------------| | `allowed_projects` | `frozenset[str]` | Project names the actor can access (min 1) | | `resource_ids` | `frozenset[str]` | Resource ULIDs in scope (empty = project-only filtering) | | `denied_resource_ids` | `frozenset[str]` | Resource ULIDs explicitly denied (denylist) | ### Methods | Method | Signature | Returns | |--------|-----------|---------| | `is_resource_in_scope` | `(resource_id: str)` | `bool` | | `effective_scope` | `()` | `frozenset[str]` — `resource_ids` minus `denied_resource_ids` | | `is_visible` | `(fragment: object)` | `bool` — fragment-level project+resource check | | `search_text` | `(backend: TextBackend, query: str, *, max_results: int = 20)` | `list[TextResult]` | | `search_vector` | `(backend: VectorBackend, embedding: list[float], *, top_k: int = 20)` | `list[VectorResult]` | | `search_graph` | `(backend: GraphBackend, query: str)` | `GraphResult` | | `from_resource_scope` | `(scope: ResourceScope)` (classmethod) | `ScopedBackendView` | **Preconditions:** - `allowed_projects` must be non-empty. - When `resource_ids` is populated, search methods raise `ScopeViolationError` if `effective_scope()` is empty (all resources denied). **Postconditions:** - Search results are restricted to in-scope resources via the `scope` parameter. - Fragments with empty `project_name` are always excluded. ## ScopedBackendSet Bundle of scoped backend views for text, vector, and graph backends. Created by `create_scoped_backend_set()` during initial context assembly. | Field | Type | Description | |-------|------|-------------| | `view` | `ScopedBackendView` | The scoped view enforcing isolation | | `text_backend` | `TextBackend \| None` | Text search backend (None if unavailable) | | `vector_backend` | `VectorBackend \| None` | Vector search backend (None if unavailable) | | `graph_backend` | `GraphBackend \| None` | Graph query backend (None if unavailable) | ### Methods | Method | Signature | Returns | |--------|-----------|---------| | `search_text` | `(query: str, *, max_results: int = 20)` | `list[TextResult]` — empty if no backend | | `search_vector` | `(embedding: list[float], *, top_k: int = 20)` | `list[VectorResult]` — empty if no backend | | `search_graph` | `(query: str)` | `GraphResult` — empty if no backend | ## ScopeViolationError Exception raised when a context request references resources outside scope. | Attribute | Type | Description | |-----------|------|-------------| | `resource_ids` | `tuple[str, ...]` | Out-of-scope resource identifiers | | `scope_project_names` | `tuple[str, ...]` | Project names that define the scope | ## ResourceAliasResolver Stateless helper that resolves resource aliases to canonical resource ULIDs. Used during scope resolution to translate user-facing resource names. | Method | Signature | Returns | |--------|-----------|---------| | `resolve` | `(project: NamespacedProject, alias_or_id: str)` | `str \| None` | | `resolve_many` | `(project: NamespacedProject, names: tuple[str, ...])` | `frozenset[str]` | | `validate_uniqueness` | `(project: NamespacedProject)` | `list[str]` — error messages for duplicates | ## Factory and Validation Functions ### `create_scoped_backend_set()` ```python def create_scoped_backend_set( scope: ResourceScope, *, text_backend: TextBackend | None = None, vector_backend: VectorBackend | None = None, graph_backend: GraphBackend | None = None, ) -> ScopedBackendSet: ... ``` ### `resolve_resource_scope()` ```python def resolve_resource_scope( projects: list[NamespacedProject], *, include_resources: tuple[str, ...] = (), exclude_resources: tuple[str, ...] = (), include_paths: tuple[str, ...] = (), exclude_paths: tuple[str, ...] = (), temporal_scope: str = "current", registry_lookup: Callable[[str], frozenset[str]] | None = None, ) -> ResourceScope: ... ``` Collects all resource IDs from linked resources across projects, then applies allowlist/denylist filtering using alias resolution. When `registry_lookup` is provided, each resource ID is expanded to include its DAG descendants (per spec §42517--42521). ### `validate_project_scope()` ```python def validate_project_scope( requested_projects: frozenset[str], available_projects: frozenset[str], ) -> None: ... # raises ScopeViolationError ``` ### `validate_resource_scope()` ```python def validate_resource_scope( requested_resource_ids: frozenset[str], scope: ResourceScope, ) -> None: ... # raises ScopeViolationError ``` ## Usage Examples ### Basic Scope Resolution ```python from cleveragents.domain.models.acms.scope_resolution import resolve_resource_scope # Resolve scope from a project — includes all linked resources scope = resolve_resource_scope([project]) # Allowlist: only include the "repo" resource (by alias) scope = resolve_resource_scope([project], include_resources=("repo",)) # Denylist: exclude "docs" resource, keep everything else scope = resolve_resource_scope([project], exclude_resources=("docs",)) # Combined: include only Python files, exclude test files scope = resolve_resource_scope( [project], include_paths=("src/**/*.py",), exclude_paths=("**/test_*", "**/*_test.py"), ) ``` ### Scoped Backend Queries ```python from cleveragents.domain.models.acms.scoped_view import ( ScopedBackendView, create_scoped_backend_set, ) # Create a scoped view from a resolved scope view = ScopedBackendView.from_resource_scope(scope) # Search with automatic scope injection results = view.search_text(text_backend, "authentication flow") vectors = view.search_vector(vector_backend, embedding) graph = view.search_graph(graph_backend, "SELECT ?s WHERE { ?s a uko:Class }") # Or use ScopedBackendSet for convenience backend_set = create_scoped_backend_set( scope, text_backend=text_backend, vector_backend=vector_backend, graph_backend=graph_backend, ) results = backend_set.search_text("authentication flow") ``` ### Scope Validation Guards ```python from cleveragents.domain.models.acms.scope_resolution import ( validate_project_scope, validate_resource_scope, ) from cleveragents.domain.models.acms.scoped_view import ScopeViolationError try: validate_project_scope( requested_projects=frozenset({"local/api", "local/frontend"}), available_projects=frozenset({"local/api"}), ) except ScopeViolationError as e: # "Projects not in scope: local/frontend. Available: local/api" print(e) ``` ## Module Locations | Module | Purpose | |--------|---------| | `cleveragents.domain.models.acms.scoped_view` | Core types: `ResourceScope`, `ScopedBackendView`, `ScopedBackendSet` | | `cleveragents.domain.models.acms.scope_resolution` | Resolution: `ResourceAliasResolver`, `resolve_resource_scope`, validation guards | | `cleveragents.domain.models.acms.tiers` | Re-exports `ScopedBackendView` for backward compat | | `cleveragents.domain.models.acms` | Package-level re-exports of all public symbols | | `cleveragents.application.services.context_tiers` | Enforcement hooks (`get_scoped`, `get_scoped_by_resource`, etc.) | ## Related - [ACMS Backends Reference](acms_backends.md) — Backend protocol definitions - [Specification: ACMS > Scoped Views](../specification.md) — Authoritative design (lines 42501--42566) - [Forgejo Issue #193](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/193) — Feature request