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
10 KiB
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_projectsmust be non-empty.- When
resource_idsis populated, search methods raiseScopeViolationErrorifeffective_scope()is empty (all resources denied).
Postconditions:
- Search results are restricted to in-scope resources via the
scopeparameter. - Fragments with empty
project_nameare 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()
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()
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()
def validate_project_scope(
requested_projects: frozenset[str],
available_projects: frozenset[str],
) -> None: ... # raises ScopeViolationError
validate_resource_scope()
def validate_resource_scope(
requested_resource_ids: frozenset[str],
scope: ResourceScope,
) -> None: ... # raises ScopeViolationError
Usage Examples
Basic Scope Resolution
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
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
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 — Backend protocol definitions
- Specification: ACMS > Scoped Views — Authoritative design (lines 42501--42566)
- Forgejo Issue #193 — Feature request