How ai-memory Handles Scope Resolution: A Deep Dive Into the ScopeResolver Architecture
ai-memory isolates data by workspace and project identifiers through a centralized ScopeResolver component that enforces strict read-only versus write-create policies.
The ai-memory repository implements a robust multi-tenant data isolation system where every API request—whether from HTTP admin routes, MCP tools, or the read-only web API—must resolve human-readable scope names into concrete internal identifiers. This resolution logic lives in a single, auditable component: the ScopeResolver. Understanding this mechanism is essential for anyone extending the system or debugging permission issues.
Core Scope Concepts
Before examining the resolution logic, you need to understand the three foundational types defined in crates/ai-memory-store/src/scope.rs:
| Type | Purpose |
|---|---|
ScopeName |
Holds the human-readable workspace and project names supplied by API callers. |
ResolvedScope |
Contains the internal WorkspaceId and ProjectId that the storage layer actually uses. |
ScopeResolutionError |
Exhaustive enum covering every failure mode: missing pairs, empty names, too many scopes, not-found errors, missing writers for create-on-write, and store failures. |
According to the ai-memory source code, these types form a clear boundary between user-facing identifiers and internal storage references. The resolver's job is to bridge this gap while maintaining strict safety invariants.
Read-Only Resolution: Failing Closed
The ScopeResolver::resolve_read_args method implements ai-memory's read-resolution policy. Located at scope.rs:54-71, this method never creates workspaces or projects—it fails closed.
The resolution precedence works as follows:
- Explicit pair – If both workspace and project are provided, resolve directly.
- Project-only with active project – If only project is supplied, check the actor's active-project pointer (when attached via
with_active_project). - Fall back to defaults – Use the resolver's configured default workspace and project.
- Reject partial pairs – Workspace-only requests without project are rejected.
// Build a read-only resolver with fallback defaults
let resolver = ScopeResolver::new(&store.reader, default_ws_id, default_proj_id)
.with_active_project(&active_project); // optional session context
// Explicit pair resolution
let scope = resolver
.resolve_read_args(Some("team"), Some("frontend"), &actor_key)
.await?;
// Project-only: falls back active-project → default
let scope = resolver
.resolve_read_args(None, Some("shared-lib"), &actor_key)
.await?;
Partial pairs trigger a ScopeResolutionError, preventing ambiguous lookups that could cross workspace boundaries.
Write-Only Resolution: Create-on-Write Semantics
For mutations, ai-memory uses ScopeResolver::resolve_write_args at scope.rs:16-33. This path allows automatic creation of missing workspaces and projects, but only when a writer is explicitly attached.
Key behaviors:
- Requires writer – The resolver must be constructed with
with_writer(&store.writer); otherwise resolution fails with a missing-writer error. - Auto-creates missing entities – Via
create_explicit_scope, missing workspaces or projects are instantiated on demand. - Respects active-project context – Unqualified project names resolve against the actor's current workspace when available.
// Upgrade to write-capable resolver
let resolver = resolver.with_writer(&store.writer);
// Creates "new-feature" project in actor's active workspace if absent
let new_scope = resolver
.resolve_write_args(None, Some("new-feature"), &actor_key)
.await?;
This create-on-write policy is documented in docs/auto-scope.md and used consistently across the server's MCP tool implementations.
Multi-Scope and Global Scope Handling
Batch Resolution Without Creation
The resolve_many_existing method handles de-duplication and validation of explicit scope lists for operations affecting multiple projects. Unlike the single-scope methods, it operates strictly in read mode—no entities are created.
Global Preferences Scope
ai-memory reserves a special "global" scope living in the default workspace. The lookup_global_scope helper at scope.rs:35-44 returns None until explicitly created via create_global_scope. This two-phase approach ensures global configuration cannot be accidentally instantiated through read paths.
Safety Guarantees and Invariants
The ScopeResolver architecture enforces critical security properties through code structure rather than convention:
| Invariant | Enforcement |
|---|---|
| Reads never create | resolve_read_args lacks store writer access |
| Writes require explicit opt-in | with_writer attachment required for creation paths |
| Partial pairs rejected | ScopeResolutionError::MissingPair for ambiguous requests |
| No implicit global creation | lookup_global_scope returns Option, create_global_scope is separate |
These guarantees align with the architecture documentation's threat model, ensuring that a compromised read-only API key cannot pollute the workspace/project namespace.
ScopeResolver Configuration and Usage Patterns
As implemented in crates/ai-memory-mcp/src/server.rs, each request constructs a fresh resolver:
// Per-request resolver instantiation pattern from the MCP server
let resolver = ScopeResolver::new(&store.reader, default_ws_id, default_proj_id);
// Attach session-specific context
let resolver = resolver.with_active_project(&actor.active_project);
// Conditionally enable writes for mutating operations
let resolver = if operation.requires_write {
resolver.with_writer(&store.writer)
} else {
resolver
};
The resolver's builder-pattern API allows precise capability restriction without proliferating parameter lists through the call stack.
Summary
- ai-memory scope resolution centralizes all workspace/project lookup logic in the
ScopeResolvercomponent. - Read operations use
resolve_read_args, which never creates entities and follows explicit → active-project → default precedence. - Write operations use
resolve_write_args, which auto-creates missing scopes only when a writer is attached. - Active-project context enables "current project" semantics without hardcoding session state into storage layer.
- Global scope follows explicit creation semantics, preventing accidental instantiation.
- Comprehensive test coverage at
scope.rs:94-180documents expected behaviors and regression cases.
Frequently Asked Questions
How does ai-memory prevent read operations from accidentally creating workspaces or projects?
The ScopeResolver maintains strict separation between read-capable and write-capable instances. The resolve_read_args method accepts only a store reader, lacking any mechanism to persist new entities. Write paths explicitly require calling with_writer(&store.writer), making create-on-write an opt-in capability that cannot be triggered through read-only codepaths.
What happens when a request specifies only a project name without a workspace?
The resolver applies a three-tier fallback: first, it checks if the actor has an active project configured (via with_active_project); if set, it uses that workspace context. Otherwise, it falls back to the resolver's configured default workspace. This design supports both fully-qualified multi-tenant requests and convenient single-project operations within a session context.
Can ai-memory handle operations across multiple projects in a single request?
Yes, through the resolve_many_existing method. This batch resolver de-duplicates scope names and validates all exist without creating any missing entities. It's designed for read-only aggregation queries or bulk operations where the caller must explicitly pre-create any needed projects through separate write requests.
Where is the ScopeResolver instantiated in the ai-memory server architecture?
The MCP server in crates/ai-memory-mcp/src/server.rs instantiates a fresh ScopeResolver per request using ScopeResolver::new(&store.reader, default_ws_id, default_proj_id). This pattern ensures each request starts with minimal capabilities, with writers and active-project context attached only when the specific operation and authentication context permits.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →