Skip to content

Separate project-scoped entity resolution from cross-project wikilink resolution #1170

Description

@phernandez

Problem

POST /v2/projects/{project_id}/knowledge/resolve currently serves two different contracts:

  1. Resolve an entity inside the project selected by an MCP/API caller.
  2. Resolve a wikilink originating in that project, where a qualified identifier such as other-project::Note may resolve to an entity owned by another project.

That makes the meaning of {project_id} unclear. For ordinary entity lookup it is the target project; for wikilink resolution it is the source/default resolution context. The route, dependency graph, and callers do not make that distinction visible.

This surfaced while adding the optional Redis read cache in #1168. The cache currently needs this guard:

cached.cacheable = result.project_external_id == project_external_id

The guard is necessary only because the project-scoped route can silently return an entity from a different project.

Current use cases and evidence

Project-scoped entity lookup

MCP edit, delete, move, and read flows construct KnowledgeClient with an active project and call this endpoint to turn a title, permalink, path, or UUID into an entity UUID.

For mutation flows, a cross-project result is not directly usable. edit_note explicitly stops before patching when the resolved entity belongs to another project and asks the caller to retry against the owning project.

Source-aware wikilink resolution

LinkResolver legitimately supports cross-project references for:

  • indexed relations such as [[other-project::Component]]
  • graph/context traversal
  • web wikilink navigation
  • relative and duplicate-title resolution using source_path

For these calls, the path project is useful: it is the source/default project for unqualified links and the project against which source_path is interpreted. A qualified identifier may override the target project.

Cloud authorization boundary

Cloud's web-v2 wikilink flow treats the path project as the source project, calls Basic Memory, then separately authorizes the project identified by resolve.project_external_id before returning a target.

The general Cloud proxy authorizes the project named in the URL before forwarding. Therefore Basic Memory must not imply that authorizing the path project is sufficient when the response may name a different target project. Any cross-project link endpoint needs an explicit target-authorization requirement for hosted integrations.

Proposed direction

Separate the two contracts instead of making one endpoint infer both meanings:

  1. Keep a strict project-scoped entity resolver for MCP/API entity operations.

    • The project in the route is the target project.
    • The resolved entity must belong to that project.
    • The owning project field may remain for compatibility, but it must equal the route project.
    • Redis caching can use one project generation without a conditional cacheability rule.
  2. Add an explicitly source-aware link resolver, for example:

    POST /v2/projects/{source_project_id}/knowledge/links/resolve
    
    • The route project is documented as source/default context.
    • source_path is interpreted within that source project.
    • Qualified references may return a different target_project_external_id.
    • Hosted callers must authorize the returned target project before exposing its metadata.
  3. Keep cross-project behavior in LinkResolver, relation resolution, context building, and wikilink navigation.

A future unscoped link endpoint could accept an optional source_project_external_id, but only fully qualified identifiers should be valid when it is absent. Unqualified identifiers and any request containing source_path require source-project context.

Internal design cleanup

The current LinkResolver is constructed with source-project repositories but can replace them internally after parsing a project prefix. This makes repository scope difficult to follow.

As part of the split, prefer a flow that:

  • parses the reference once
  • represents source context and any explicit target separately
  • selects the target project once
  • resolves within one explicit project scope
  • returns both entity identity and owning-project identity as a typed result

The public endpoint should not depend on callers understanding that a project-bound resolver may silently switch repositories.

Acceptance criteria

  • Project-scoped entity resolution never returns an entity owned by another project.
  • Cross-project wikilinks continue to resolve through a separately named source-aware contract.
  • Requests with source_path have an explicit source project.
  • Qualified cross-project results expose the target project UUID.
  • Cloud/hosted integration requirements explicitly require authorization of the resolved target project.
  • MCP read/edit/move/delete callers have tests demonstrating their intended routing behavior.
  • Core API tests distinguish same-project entity lookup from cross-project link resolution.
  • Relation resolution and graph/context traversal retain cross-project coverage.
  • Redis read-cache keys and invalidation scopes follow the clarified endpoint contracts.
  • The legacy endpoint remains compatible or has a documented migration path.

Test changes to consider

  • Move the existing API cross-project resolve test to the new link-resolution endpoint.
  • Add an invariant test that the entity resolver cannot escape its route project.
  • Retain LinkResolver service tests for project::note.
  • Retain Cloud web-v2 tests that authorize the returned target project.
  • Add hosted integration coverage ensuring a source-project authorization does not substitute for target-project authorization.

Out of scope

This issue should not expand #1168. The Redis cache PR can continue skipping cross-project resolve results until the API contracts are separated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    arch-review2026-07 architecture review cleanup (cloud tracker basic-memory-cloud#1558)cloudBasic Memory CloudenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions