forked from HAL9000/cleveragents-core
b028c80cab
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
272 lines
10 KiB
Markdown
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
|