Files
temp/docs/reference/scoped_backend_view.md
hamza.khyari b028c80cab feat(acms): add scoped backend view filtering
Add project-resource isolation to the ACMS context tier system via
ScopedBackendView, ResourceScope, alias resolution, DAG expansion,
and enforcement hooks.

Core types (scoped_view.py): ResourceScope, ScopeViolationError,
ScopedBackendView, ScopedBackendSet, create_scoped_backend_set.

Scope resolution (scope_resolution.py): ResourceAliasResolver,
validate_project_scope, validate_resource_scope, resolve_resource_scope
with registry_lookup callback for DAG expansion.

Tier integration (scoped_tiers.py): ScopedTierMixin with get_scoped,
get_scoped_by_resource, validate_fragment_scope, store_with_scope_check,
get_scoped_metrics mixed into ContextTierService.

Backward compat: tiers.py re-exports ScopedBackendView.

Tests: 74 Behave scenarios (235 steps), 8 Robot Framework cases,
7 ASV benchmark suites. Lint and typecheck clean.

ISSUES CLOSED: #193
2026-03-06 00:49:33 +00:00

272 lines
10 KiB
Markdown

# 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