# How Workspace, Project, and Path Scopes Are Resolved in ai-memory

> Understand how ai-memory resolves workspace, project, and path scopes via the centralized ScopeResolver. Learn about its fail-closed policies and explicit parameter enforcement.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: internals
- Published: 2026-09-11

---

**The ai-memory system resolves workspace, project, and path scopes through a centralized `ScopeResolver` that evaluates a three-tuple `(workspace_id, project_id, path)` at the entry points of every HTTP route and MCP tool, enforcing fail-closed policies where reads never create objects and writes require explicit parameters or an active-project pointer.**

ai-memory isolates data using a strict hierarchical namespace defined by workspace, project, and path components. All public surfaces—including HTTP admin routes, MCP tools, and the read-only web API—delegate scope resolution to a single `ScopeResolver` implementation found in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs). This design ensures consistent behavior across the stack, whether the request originates from a Model Context Protocol (MCP) tool invocation or a direct API call.

## Core Scope Concepts: Workspace, Project, and Path

Scope resolution in ai-memory operates on three fundamental primitives defined in [`crates/ai-memory-core/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/lib.rs):

| Concept | Description | Implementation |
| --- | --- | --- |
| **Workspace** | Logical container aggregating multiple projects. | `WorkspaceId` in `ai_memory_core` |
| **Project** | Sub-container inside a workspace that holds pages and observations. | `ProjectId` in `ai_memory_core` |
| **Path** | Relative file path inside a project (e.g., a markdown page). | Handled by the wiki layer in [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) |

The combination of these three elements forms a unique address `(workspace_id, project_id, path)` that identifies any resource in the system.

## ScopeResolver Construction and Configuration

The `ScopeResolver` is instantiated per request and configured with the necessary dependencies to perform lookups or create new entries. According to the constructor at lines 24–31 of [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs), the resolver requires:

- A **read pool** (`&ReaderPool`) for lookups that never create objects.
- An optional **writer handle** (`Option<&WriterHandle>`) required only for write operations that may create workspaces or projects.
- An optional **active-project map** (`Option<&ActiveProject>`) containing the per-actor "current project" pointer populated by lifecycle hooks.
- The **default workspace and project** IDs used as server-wide fallbacks.

```rust
let resolver = ScopeResolver::new(&store.reader, default_ws, default_proj)
    .with_writer(&store.writer)           // for writes that can create
    .with_active_project(&active_project); // for current-project defaults

```

All public surfaces delegate to this resolver, ensuring a single, well-defined policy governs scope resolution across the entire application.

## Read-Side Resolution Policy (`resolve_read_args`)

For read operations, the `resolve_read_args` method (lines 68–82 in [`scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/scope.rs)) follows a strict algorithm that never creates new workspace or project entries:

1. **Explicit pair validation** – If both `workspace` and `project` names are supplied, the resolver calls `lookup_existing` to verify the pair exists. This path is read-only and never creates objects.
2. **Partial pair rejection** – If only one of the two identifiers is present, the request immediately fails with `WorkspaceProjectPairRequired`.
3. **Default resolution** – When no explicit scope is provided, the resolver delegates to `resolve_current_or_project`, which:
   - Attempts to use the **actor's active-project pointer** via `ActiveProject::get_for_read`.
   - If a project name is supplied without a workspace, it searches the active workspace first, then falls back to the default workspace.
   - If no project name is provided, it returns the active pointer or the default `(default_workspace_id, default_project_id)`.

Missing objects in read paths result in a `NotFound` error; the system never auto-creates scopes during read operations.

## Write-Side Resolution Policy (`resolve_write_args`)

Write resolution, implemented in `resolve_write_args` (lines 33–84), differs significantly from read resolution in two key ways: it allows creation of missing scopes and enforces stricter ambiguity checks.

When an explicit `workspace`/`project` pair is supplied, the resolver invokes `create_explicit_scope` via the writer handle, which atomically creates missing workspace or project entries. For **unscoped writes** where neither identifier is provided:

1. The resolver looks up the **active-project pointer** using `ActiveProject::lookup_for`.
2. If the pointer resolves to a valid workspace/project pair, that scope is used.
3. If the pointer references a non-existent project (mismatch), the operation fails with `AmbiguousUnscopedWrite`, preventing silent mis-filing of data.
4. If no pointer is set, the system defaults to the global `(default_workspace_id, default_project_id)`.

The writer handle must be present for all write operations; otherwise, the resolver returns `WriterRequired`.

## Scope Resolution Helpers

The `ScopeResolver` exposes several helper functions used by administrative routes and batch operations, all defined in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs):

- **`lookup_existing_scope`** – Locates an existing workspace/project pair without creating entries. Used extensively by admin routes that require validation without mutation.
- **`create_explicit_scope`** – The only code path capable of writing new `WorkspaceId` or `ProjectId` entries to the store. Called exclusively during write resolution when explicit names are provided.
- **`resolve_many_existing`** – De-duplicates and validates a list of explicit scopes against the database. It fails closed if any entry is missing or if the list exceeds a configurable limit.

These helpers are exercised by the comprehensive test suite, including cases such as `read_args_reject_partial_scope`, `project_only_read_prefers_active_workspace_then_default`, and `write_args_create_project_in_active_workspace`.

## Path Resolution and the Wiki Layer

Once the `(workspace_id, project_id)` tuple is resolved, the **path** component is processed by the wiki layer. In [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs), the `Wiki::write_page` and `Wiki::apply_batch` methods receive a `PagePath` (relative to the project root) and enforce atomic writes, attribution tracking, and search indexing.

The read API in [`crates/ai-memory-web/src/routes/page.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-web/src/routes/page.rs) uses the resolved scope to locate the actual markdown file on disk under `data/<workspace>/<project>/<path>.md`. Thus, the complete triple `(workspace_id, project_id, path)` uniquely identifies any page or observation in the system, with the `ScopeResolver` handling the first two components and the wiki layer handling the final path resolution.

## Summary

- **Centralized Resolution** – All HTTP routes and MCP tools delegate to `ScopeResolver` in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs) to ensure consistent policy enforcement.
- **Read vs. Write Semantics** – Reads use `resolve_read_args` and never create objects; writes use `resolve_write_args` and can auto-create scopes only when explicitly requested.
- **Fail-Closed Design** – Partial scope pairs are rejected, ambiguous unscoped writes fail with `AmbiguousUnscopedWrite`, and missing objects return `NotFound` rather than defaulting to creation.
- **Active-Project Pointer** – The `ActiveProject` type provides per-actor default scopes, checked after explicit parameters but before server-wide defaults.
- **Path Handling** – The wiki layer resolves relative paths only after the workspace and project IDs are validated, storing files under `data/<workspace>/<project>/<path>.md`.

## Frequently Asked Questions

### What happens if I provide only a workspace name without a project name in ai-memory?

The request is immediately rejected with the `WorkspaceProjectPairRequired` error. The resolver requires both identifiers together or neither; partial scope specifications are not permitted to prevent ambiguous targeting.

### How does ai-memory handle unscoped write operations when no explicit scope is provided?

For unscoped writes, the resolver first checks the actor's **active-project pointer** via `ActiveProject::lookup_for`. If the pointer resolves to a valid existing project, that scope is used. If the pointer is unset, the system falls back to the default workspace and project. If the pointer points to a non-existent project, the write fails with `AmbiguousUnscopedWrite` to prevent data mis-filing.

### What is the difference between `resolve_read_args` and `resolve_write_args` in the ScopeResolver?

`resolve_read_args` (lines 68–82) is strictly read-only and fails if the requested workspace or project does not exist, utilizing only the read pool. `resolve_write_args` (lines 33–84) requires a writer handle, can create missing workspaces or projects via `create_explicit_scope`, and implements additional logic to validate the active-project pointer for unscoped operations.

### Where are the core type definitions for WorkspaceId and ProjectId located?

The core type definitions reside in [`crates/ai-memory-core/src/lib.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/lib.rs), which exports `WorkspaceId` and `ProjectId` as the fundamental primitives for scope identification. These types are consumed by the store layer, the wiki layer, and all API routes to maintain type safety across the workspace, project, and path hierarchy.