Files
hamza.khyari b028c80cab
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / build (pull_request) Successful in 15s
CI / quality (pull_request) Successful in 18s
CI / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 41s
CI / unit_tests (pull_request) Successful in 2m29s
CI / integration_tests (pull_request) Successful in 3m2s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 4m28s
CI / benchmark-regression (pull_request) Successful in 29m5s
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

466 lines
22 KiB
Gherkin

Feature: Scoped Backend View Filtering
As a developer
I want scoped backend views that filter text/vector/graph queries to project resources
So that plans never see resources outside their scope (cross-project isolation)
# ---- ResourceScope ----
Scenario: Create a valid ResourceScope
Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
Then sv the scope should contain resource "RES01"
And sv the scope should contain resource "RES02"
And sv the scope should not contain resource "RES99"
And sv the scope should contain project "local/api"
And sv the scope should not contain project "local/frontend"
Scenario: ResourceScope is frozen
Given sv a ResourceScope with resources "RES01" and projects "local/api"
Then sv modifying the scope should raise an error
Scenario: ResourceScope temporal_scope defaults to current
Given sv a ResourceScope with resources "RES01" and projects "local/api"
Then sv temporal_scope should be "current"
Scenario: ResourceScope rejects invalid temporal_scope
Then sv creating a scope with temporal_scope "invalid" should raise ValidationError
Scenario: ResourceScope allows empty resource_ids after filtering
Then sv creating a scope with no resources should succeed
Scenario: ResourceScope rejects empty project_names
Then sv creating a scope with no projects should raise ValidationError
# ---- ResourceScope path filtering ----
Scenario: ResourceScope matches_path with include patterns
Given sv a ResourceScope with include_paths "src/**/*.py", "tests/**/*.py"
Then sv path "src/main.py" should match
And sv path "tests/test_foo.py" should match
And sv path "docs/readme.md" should not match
Scenario: ResourceScope matches_path with exclude patterns
Given sv a ResourceScope with exclude_paths "**/node_modules/**", "**/__pycache__/**"
Then sv path "src/main.py" should match
And sv path "node_modules/foo/bar.js" should not match
And sv path "src/__pycache__/foo.pyc" should not match
Scenario: ResourceScope matches_path with both include and exclude
Given sv a ResourceScope with include_paths "src/**/*.py" and exclude_paths "**/test_*"
Then sv path "src/main.py" should match
And sv path "src/test_main.py" should not match
And sv path "docs/readme.md" should not match
# ---- ScopedBackendView: fragment-level filtering ----
Scenario: ScopedBackendView filters fragments by project name
Given sv a project-only ScopedBackendView for "proj-a"
And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
And sv a fragment "frag-b" in project "proj-b" with resource "RES02"
Then sv fragment "frag-a" should be visible
And sv fragment "frag-b" should not be visible
Scenario: ScopedBackendView excludes fragments with empty project_name
Given sv a project-only ScopedBackendView for "proj-a"
And sv a fragment "frag-x" with no project
Then sv fragment "frag-x" should not be visible
Scenario: ScopedBackendView filters by resource_ids when set
Given sv a project-and-resource ScopedBackendView for "proj-a" resources "RES01"
And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
And sv a fragment "frag-c" in project "proj-a" with resource "RES99"
Then sv fragment "frag-a" should be visible
And sv fragment "frag-c" should not be visible
Scenario: ScopedBackendView denied_resource_ids blocks specific resources
Given sv a deny-resource ScopedBackendView for "proj-a" allow "RES01", "RES02" deny "RES02"
And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
And sv a fragment "frag-d" in project "proj-a" with resource "RES02"
Then sv fragment "frag-a" should be visible
And sv fragment "frag-d" should not be visible
# ---- ScopedBackendView: backend-level search proxying ----
Scenario: ScopedBackendView proxies text search with scope injected
Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
And sv a ScopedBackendView from that scope
And sv an InMemoryTextBackend
When sv I search text for "auth flow"
Then sv the text search should return an empty list
And sv the text search should not raise an error
Scenario: ScopedBackendView proxies vector search with scope injected
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendView from that scope
And sv an InMemoryVectorBackend
When sv I search vector with embedding
Then sv the vector search should return an empty list
Scenario: ScopedBackendView proxies graph search with scope injected
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendView from that scope
And sv an InMemoryGraphBackend
When sv I search graph for "SELECT ?s WHERE { ?s a uko:Container }"
Then sv the graph search should return an empty GraphResult
# ---- ScopedBackendView: effective_scope ----
Scenario: effective_scope returns resource_ids minus denied
Given sv a triple-resource ScopedBackendView for "p" with "R1", "R2", "R3" denying "R2"
Then sv effective_scope should contain "R1" and "R3" but not "R2"
Scenario: effective_scope is empty when all resources denied
Given sv a single-deny ScopedBackendView for "p" allow "R1" deny "R1"
Then sv effective_scope should be empty
# ---- ScopedBackendView: from_resource_scope ----
Scenario: from_resource_scope creates a view with scope's IDs and projects
Given sv a ResourceScope with two resources "RES01", "RES02" and two projects "local/api", "local/web"
When sv I create a ScopedBackendView from that ResourceScope
Then sv the view should allow project "local/api"
And sv the view should allow project "local/web"
And sv the view resource_ids should contain "RES01" and "RES02"
# ---- ScopedBackendSet ----
Scenario: ScopedBackendSet wraps multiple backends
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendSet with text, vector, and graph backends
When sv I search text via backend set for "test query"
Then sv the backend set text search should succeed
Scenario: ScopedBackendSet with no text backend returns empty list
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendSet with no text backend
When sv I search text via backend set for "test query"
Then sv the backend set text search should return empty list
# ---- ScopeViolationError ----
Scenario: ScopeViolationError carries resource and project info
When sv I raise a ScopeViolationError with resource "RES01" and projects "proj-a"
Then sv the error resource_ids should contain "RES01"
And sv the error scope_project_names should contain "proj-a"
# ---- validate_project_scope ----
Scenario: validate_project_scope passes for valid projects
Then sv validating projects "proj-a" against available "proj-a", "proj-b" should pass
Scenario: validate_project_scope rejects out-of-scope projects
Then sv validating projects "proj-c" against available "proj-a", "proj-b" should raise ScopeViolationError
Scenario: validate_project_scope rejects empty requested projects
Then sv validating empty requested projects should raise ValueError
# ---- validate_resource_scope ----
Scenario: validate_resource_scope passes for in-scope resources
Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
Then sv validating resources "RES01" against that scope should pass
Scenario: validate_resource_scope rejects out-of-scope resources
Given sv a ResourceScope with resources "RES01" and projects "local/api"
Then sv validating resources "RES99" against that scope should raise ScopeViolationError
# ---- ResourceAliasResolver ----
Scenario: Resolve alias to resource_id
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
And sv a ResourceAliasResolver
When sv I resolve alias "repo"
Then sv the resolved resource_id should be "01HQ8ZDRX50000000000000001"
Scenario: Resolve by resource_id directly
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
And sv a ResourceAliasResolver
When sv I resolve alias "01HQ8ZDRX50000000000000001"
Then sv the resolved resource_id should be "01HQ8ZDRX50000000000000001"
Scenario: Resolve unknown alias returns None
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
And sv a ResourceAliasResolver
When sv I resolve alias "nonexistent"
Then sv the resolved resource_id should be None
Scenario: Resolve rejects empty input
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
And sv a ResourceAliasResolver
Then sv resolving empty alias should raise ValueError
Scenario: resolve_many resolves multiple aliases
Given sv a project "local/myproj" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
And sv a ResourceAliasResolver
When sv I resolve many aliases "repo", "docs"
Then sv the resolved set should contain "01HQ8ZDRX50000000000000001" and "01HQ8ZDRX50000000000000002"
Scenario: validate_uniqueness detects duplicate aliases
Given sv a project "local/myproj" with duplicate alias "repo" on "01HQ8ZDRX50000000000000001" and "01HQ8ZDRX50000000000000002"
And sv a ResourceAliasResolver
When sv I validate alias uniqueness
Then sv there should be 1 uniqueness error mentioning "repo"
Scenario: validate_uniqueness passes for unique aliases
Given sv a project "local/myproj" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
And sv a ResourceAliasResolver
When sv I validate alias uniqueness
Then sv there should be 0 uniqueness errors
# ---- resolve_resource_scope ----
Scenario: resolve_resource_scope collects all resources from projects
Given sv a project "local/api" with linked resources "01HQ8ZDRX50000000000000001", "01HQ8ZDRX50000000000000002"
When sv I resolve the resource scope
Then sv the scope should have 2 resource_ids
And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
And sv the scope should contain resource "01HQ8ZDRX50000000000000002"
Scenario: resolve_resource_scope applies include allowlist
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
When sv I resolve the resource scope with include "repo"
Then sv the scope should have 1 resource_ids
And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
Scenario: resolve_resource_scope applies exclude denylist
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
When sv I resolve the resource scope with exclude "docs"
Then sv the scope should have 1 resource_ids
And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
Scenario: resolve_resource_scope rejects empty projects
Then sv resolving scope with empty projects should raise ValueError
# ---- ContextTierService: scope enforcement ----
Scenario: get_scoped_by_resource filters by ResourceScope
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "f1" project "proj-a" resource "RES01" tier "hot"
And sv I store fragment "f2" project "proj-b" resource "RES02" tier "hot"
Then sv get_scoped_by_resource should return 1 fragment
Scenario: store_with_scope_check rejects out-of-scope resource
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
Then sv storing fragment with resource "RES99" should raise ScopeViolationError
Scenario: store_with_scope_check rejects out-of-scope project
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
Then sv storing fragment with project "evil-proj" and resource "RES01" should raise ScopeViolationError
Scenario: validate_fragment_scope passes for in-scope fragment
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "f3" project "proj-a" resource "RES01" tier "hot"
Then sv validating fragment "f3" against the scope should pass
Scenario: validate_fragment_scope rejects out-of-scope fragment
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "f4" project "proj-b" resource "RES02" tier "hot"
Then sv validating fragment "f4" against the scope should raise ScopeViolationError
# ---- ScopedBackendSet: vector and graph ----
Scenario: ScopedBackendSet proxies vector search
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendSet with text, vector, and graph backends
When sv I search vector via backend set
Then sv the backend set vector search should return empty list
Scenario: ScopedBackendSet proxies graph search
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendSet with text, vector, and graph backends
When sv I search graph via backend set for "SELECT ?s WHERE { ?s a uko:X }"
Then sv the backend set graph search should return empty GraphResult
# ---- Mixed project scope guard ----
Scenario: Mixed project scope guard catches unlinked projects
Then sv validating mixed projects "proj-a", "proj-c" against available "proj-a", "proj-b" should raise ScopeViolationError
# ---- Coverage: field validator coercion (scoped_view L226, L237) ----
Scenario: ScopedBackendView coerces list resource_ids to frozenset
Then sv constructing view with list resource_ids should succeed
Scenario: ScopedBackendView coerces list denied_resource_ids to frozenset
Then sv constructing view with list denied_resource_ids should succeed
# ---- Coverage: effective_scope project-only (scoped_view L273) ----
Scenario: effective_scope returns empty for project-only view
Given sv a project-only ScopedBackendView for "proj-a"
Then sv effective_scope should be empty
# ---- Coverage: _require_effective_scope raises (scoped_view L326-330) ----
Scenario: search_text raises when all resources are denied
Given sv a single-deny ScopedBackendView for "p" allow "R1" deny "R1"
And sv an InMemoryTextBackend
Then sv searching text via denied view should raise ScopeViolationError
# ---- Coverage: ScopedBackendSet None backends (scoped_view L489, L505) ----
Scenario: ScopedBackendSet search_vector with no vector backend
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendSet with no vector backend
Then sv searching vector via no-vector set should return empty list
Scenario: ScopedBackendSet search_graph with no graph backend
Given sv a ResourceScope with resources "RES01" and projects "local/api"
And sv a ScopedBackendSet with no graph backend
Then sv searching graph via no-graph set should return empty GraphResult
# ---- Coverage: resolve_many unresolvable alias (scoped_view L617) ----
Scenario: resolve_many skips unresolvable aliases
Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
And sv a ResourceAliasResolver
When sv I resolve many aliases "repo", "nonexistent"
Then sv the resolved set size should be 1
And sv the resolved set should contain "01HQ8ZDRX50000000000000001"
# ---- Coverage: validate_uniqueness no-alias branch (scoped_view L638) ----
Scenario: validate_uniqueness ignores resources without aliases
Given sv a project with aliased and unaliased resources
And sv a ResourceAliasResolver
When sv I validate alias uniqueness
Then sv there should be 0 uniqueness errors
# ---- Coverage: validate_project_scope empty available (scoped_view L676) ----
Scenario: validate_project_scope rejects empty available projects
Then sv validating projects against empty available should raise ValueError
# ---- Coverage: validate_resource_scope empty request (scoped_view L710) ----
Scenario: validate_resource_scope passes for empty requested resources
Given sv a ResourceScope with resources "RES01" and projects "local/api"
Then sv validating empty resources against scope should pass
# ---- Coverage: resolve_resource_scope empty after filter (scoped_view L790) ----
Scenario: resolve_resource_scope produces empty after include filtering
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
When sv I resolve the resource scope with include "nonexistent"
Then sv the scope should have 0 resource_ids
# ---- Coverage: context_tiers store empty id (L97) ----
Scenario: store rejects empty fragment_id
Given sv a ContextTierService
Then sv storing fragment with empty id should raise ValueError
# ---- Coverage: context_tiers get_scoped_view empty (L191) ----
Scenario: get_scoped_view with empty project_names returns empty
Given sv a ContextTierService
Then sv get_scoped_view with empty projects should return empty list
# ---- Coverage: context_tiers evict_lru empty store (L269) ----
Scenario: evict_lru on empty tier returns empty list
Given sv a ContextTierService
Then sv evict_lru on empty cold tier should return empty list
# ---- Coverage: context_tiers get_scoped_metrics (L311-316) ----
Scenario: get_scoped_metrics counts fragments by project
Given sv a ContextTierService
When sv I store fragment "m1" project "proj-a" resource "RES01" tier "hot"
And sv I store fragment "m2" project "proj-b" resource "RES02" tier "warm"
Then sv get_scoped_metrics for "proj-a" should show 1 hot and 0 warm
# ---- Coverage: context_tiers get_scoped_by_resource 0 rejected (L355) ----
Scenario: get_scoped_by_resource returns all when none rejected
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "g1" project "proj-a" resource "RES01" tier "hot"
Then sv get_scoped_by_resource should return 1 fragment
# ---- Coverage: context_tiers validate_fragment_scope not found (L382) ----
Scenario: validate_fragment_scope rejects non-existent fragment
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
Then sv validating non-existent fragment should raise ValueError
# ---- Coverage: context_tiers store_with_scope_check happy path (L434) ----
Scenario: store_with_scope_check stores in-scope fragment successfully
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
Then sv store_with_scope_check should accept in-scope fragment
# ---- Coverage: context_tiers budget property (L443) ----
Scenario: tier budget property returns TierBudget
Given sv a ContextTierService
Then sv tier service budget should be a TierBudget
# ---- Coverage: context_tiers get_for_actor no project filter (L161) ----
Scenario: get_for_actor without project filter returns fragments
Given sv a ContextTierService
When sv I store fragment "a1" project "proj-a" resource "RES01" tier "hot"
Then sv get_for_actor without projects should return a list
# ---- Coverage: context_tiers _summarize_for_cold false branch (L481) ----
Scenario: demote warm fragment with short content to cold
Given sv a ContextTierService
When sv I store a short warm fragment "sf1"
Then sv demoting "sf1" to cold should keep content intact
# ---- P0-S1: is_visible denies empty resource_id when resource_ids set ----
Scenario: is_visible denies fragment with empty resource_id when resource filtering active
Given sv a ScopedBackendView for projects "proj-a" with resource_ids "RES01"
When sv I check visibility of a fragment with project "proj-a" and empty resource_id
Then sv the fragment should NOT be visible
Scenario: is_visible allows fragment with empty resource_id when no resource filtering
Given sv a ScopedBackendView for project "proj-a"
When sv I check visibility of a fragment with project "proj-a" and empty resource_id
Then sv the fragment should be visible
# ---- P0-S2: get_scoped enforces scope ----
Scenario: get_scoped returns fragment when in scope
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "f1" project "proj-a" resource "RES01" tier "hot"
Then sv get_scoped for "f1" should return the fragment
Scenario: get_scoped returns None for out-of-scope fragment
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "f2" project "proj-b" resource "RES02" tier "hot"
Then sv get_scoped for "f2" should return None
# ---- P1-SPEC1: DAG expansion via registry_lookup ----
Scenario: resolve_resource_scope expands child resources via registry_lookup
Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
When sv I resolve scope with registry_lookup expanding "01HQ8ZDRX50000000000000001" to "01HQ8ZDRX50000000000000099"
Then sv the scope should have 3 resource_ids
And sv the scope should contain resource "01HQ8ZDRX50000000000000099"
# ---- P2-B2: tuple coercion in field validators ----
Scenario: ScopedBackendView accepts tuple for allowed_projects
When sv I create a ScopedBackendView with tuple allowed_projects
Then sv the view should have 1 allowed project
# ---- P0-S2: get_scoped with empty resource_id denied ----
Scenario: get_scoped denies fragment with empty resource_id when scope has resources
Given sv a ContextTierService
And sv a ResourceScope with resources "RES01" and projects "proj-a"
When sv I store fragment "f3" with project "proj-a" and empty resource_id in tier "hot"
Then sv get_scoped for "f3" should return None