Problem
POST /v2/projects/{project_id}/knowledge/resolve currently serves two different contracts:
- Resolve an entity inside the project selected by an MCP/API caller.
- 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:
-
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.
-
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.
-
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
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.
Problem
POST /v2/projects/{project_id}/knowledge/resolvecurrently serves two different contracts:other-project::Notemay 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:
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
KnowledgeClientwith 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_noteexplicitly 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
LinkResolverlegitimately supports cross-project references for:[[other-project::Component]]source_pathFor these calls, the path project is useful: it is the source/default project for unqualified links and the project against which
source_pathis 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_idbefore 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:
Keep a strict project-scoped entity resolver for MCP/API entity operations.
Add an explicitly source-aware link resolver, for example:
source_pathis interpreted within that source project.target_project_external_id.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 containingsource_pathrequire source-project context.Internal design cleanup
The current
LinkResolveris 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:
The public endpoint should not depend on callers understanding that a project-bound resolver may silently switch repositories.
Acceptance criteria
source_pathhave an explicit source project.Test changes to consider
LinkResolverservice tests forproject::note.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.