Skip to content

Add workspace namespace isolation#982

Open
scottgl9 wants to merge 2 commits into
rohitg00:mainfrom
scottgl9:feature/workspace-namespaces-from-main
Open

Add workspace namespace isolation#982
scottgl9 wants to merge 2 commits into
rohitg00:mainfrom
scottgl9:feature/workspace-namespaces-from-main

Conversation

@scottgl9

@scottgl9 scottgl9 commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Add a first-class workspace namespace boundary that is separate from project.

This introduces:

  • AGENTMEMORY_NAMESPACE
  • AGENTMEMORY_NAMESPACE_SCOPE=shared|isolated

and wires namespace-aware behavior across the core read/write surfaces.

Closes #981.

Why

project scoping is useful, but it is not the same thing as a top-level workspace/environment boundary.

In shared-daemon setups, users may need multiple isolated environments such as:

  • work
  • personal
  • research
  • client/customer-specific spaces

The same project slug can exist in more than one workspace, and project-only filtering is not a strong enough model for that.

This change makes:

  • namespace = workspace boundary
  • project = project identifier inside that workspace

What changed

  • Added namespace env/config loading in src/config.ts
  • Added namespace to sessions, observations, memories, lessons, and profiles
  • Stamped namespace through write paths including session start, observe, remember, LLM compression, and synthetic compression
  • Enforced namespace filtering in:
    • mem::search
    • mem::smart-search
    • mem::context
    • mem::enrich
    • /agentmemory/sessions
    • /agentmemory/observations
    • /agentmemory/memories
  • Keyed project profiles by namespace + project to avoid collisions
  • Extended lessons to avoid cross-namespace fingerprint collisions
  • Documented the feature in README.md and .env.example
  • Added focused namespace regression tests

Validation

Validated with:

  • focused namespace regression tests passing
  • npm run build passing
  • full unit suite otherwise green except for one pre-existing unrelated fs-watcher failure

Summary by CodeRabbit

  • New Features

    • Added workspace namespace support to isolate sessions, observations, memories, profiles, lessons, and search results.
    • Implemented shared vs isolated namespace modes, plus namespace="*" wildcard handling for list-style access.
    • Extended API operations (including session start, observe, context, search, smart-search, enrich, remember, profile, and lesson endpoints) to accept an optional namespace.
  • Bug Fixes

    • Fixed cross-namespace leakage by enforcing namespace-aware filtering for context, search, enrich, memory selection, and export/import.
  • Documentation

    • Updated .env.example and README with workspace namespace configuration guidance.
  • Tests

    • Added/expanded namespace isolation test coverage.

@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

@scottgl9 is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds workspace namespaces as a separate scope from project, threads namespace through config, keys, APIs, and read/write flows, and updates docs and tests for isolated and wildcard namespace behavior.

Changes

Workspace namespace isolation

Layer / File(s) Summary
Namespace contract
.env.example, README.md, src/config.ts, src/utils/namespace.ts, src/types.ts
Namespace configuration, helper functions, and optional namespace fields are added across shared types and docs.
Write stamping
src/functions/compress-synthetic.ts, src/functions/compress.ts, src/state/memory-utils.ts, src/functions/observe.ts, src/functions/remember.ts, src/functions/lessons.ts
Compressed observations, sessions, memories, and saved lessons carry namespace, and memory supersession checks namespace before replacing an existing record.
Profile keying
src/functions/profile.ts, src/functions/export-import.ts, src/functions/claude-bridge.ts
Project profiles are read, written, exported, imported, and looked up with makeProjectProfileKey(project, namespace).
Search filtering
src/functions/search.ts, src/functions/smart-search.ts
mem::search and mem::smart-search accept namespace input and apply namespace-aware filtering in the main, fallback, and trimmed result paths.
Context and enrich reads
src/functions/enrich.ts, src/functions/context.ts
mem::enrich and mem::context normalize namespace, filter related data by namespace, and include namespace in context output and logging.
Lesson filtering
src/functions/lessons.ts
Lesson save, recall, and list paths include namespace in identities and filters.
API wiring
src/triggers/api.ts, src/triggers/events.ts
REST and trigger handlers normalize namespace inputs, default isolated requests to the configured namespace, and forward namespace into downstream mem::* calls and session records.
API list filters
src/triggers/api.ts
Session, observation, and memory list endpoints support namespace query params, wildcard bypasses, and stored-namespace filtering.
API isolation tests
test/api-namespace-isolation.test.ts
New API tests cover isolated defaults and wildcard overrides for session start, sessions, and memories.
Core isolation tests
test/agent-isolation-search.test.ts, test/cross-project-isolation.test.ts, test/namespace-isolation.test.ts
Existing isolation mocks and the new namespace suite validate search, enrich, profile, and context behavior across namespaces.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ApiSessionStart as api::session::start
  participant MemContext as mem::context
  participant KVSessions as KV.sessions

  Client->>ApiSessionStart: start session with namespace
  ApiSessionStart->>KVSessions: store Session(namespace)
  ApiSessionStart->>MemContext: sessionId, project, namespace
  MemContext->>KVSessions: load sessions by project and namespace
  MemContext-->>ApiSessionStart: context result
  ApiSessionStart-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rohitg00/agentmemory#654: Adds agentId-based isolation in the same shared read/write paths, including mem::remember, mem::observe, and mem::smart-search.
  • rohitg00/agentmemory#849: Changes mem::search and related API handling to filter by agent scope, overlapping with the namespace search isolation paths here.
  • rohitg00/agentmemory#662: Updates project-scoped filtering on overlapping read paths such as mem::enrich and mem::search.

Suggested reviewers

  • rohitg00

Poem

I hopped through work and personal lanes, 🐇
With namespace tags like moonlit grains.
One burrow, many gardens—neat and bright,
Search kept its carrots in the right light.
Hooray for cozy scopes tonight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed It clearly summarizes the main change: adding workspace namespace isolation.
Linked Issues check ✅ Passed It covers #981 by adding namespace writes, namespace-aware reads/searches/context, composite profile keys, lesson scoping, and regression tests.
Out of Scope Changes check ✅ Passed The changes stay focused on namespace isolation, docs, config, and tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Introduce a first-class namespace/workspace boundary that is separate from the existing project field.

This adds AGENTMEMORY_NAMESPACE and AGENTMEMORY_NAMESPACE_SCOPE, stamps namespace onto sessions, observations, memories, lessons, and project profiles, and enforces namespace-aware filtering across the core recall and API surfaces.

Key behavior changes:
- add namespace env/config loading with shared vs isolated modes
- stamp namespace on write paths including session start, observe, remember, compress, and synthetic compression
- enforce namespace filtering in mem::search, mem::smart-search, mem::context, and mem::enrich
- filter REST list/read endpoints for sessions, observations, and memories in isolated mode
- keep project as an intra-namespace identifier instead of the top-level workspace boundary
- key project profiles by namespace+project so the same project slug can exist in multiple workspaces without collisions
- extend lessons to carry namespace and avoid cross-namespace fingerprint collisions
- document the feature in README and .env.example

Validation:
- focused namespace regression tests passed
- build passed with npm run build
- full unit suite was green except for one pre-existing unrelated fs-watcher test failure
@scottgl9 scottgl9 force-pushed the feature/workspace-namespaces-from-main branch from 75408f3 to e159a48 Compare June 26, 2026 18:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/functions/smart-search.ts (1)

226-231: 🔒 Security & Privacy | 🟠 Major

Lessons returned by smart-search bypass namespace isolation

The mem::lesson-recall function supports a namespace parameter, but the local recallLessons helper in src/functions/smart-search.ts does not accept or forward it. Consequently, even when filterNamespace is applied to the main search query, the lessons retrieved via recallLessons ignore this scope, allowing data from other namespaces to leak into results.

Update recallLessons to accept and pass the namespace:

Proposed fix
  async function recallLessons(
    sdk: ISdk,
    query: string,
    limit: number,
    project?: string,
+   namespace?: string,
  ): Promise<CompactLessonResult[]> {
    try {
      const result = (await sdk.trigger({
        function_id: "mem::lesson-recall",
-       payload: { query, limit, project },
+       payload: { query, limit, project, namespace },
      })) as { success?: boolean; lessons?: Array<Lesson & { score?: number }>; };

Update the invocation in src/functions/smart-search.ts:

-          ? recallLessons(sdk, data.query, lessonLimit, data.project)
+          ? recallLessons(sdk, data.query, lessonLimit, data.project, filterNamespace)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/smart-search.ts` around lines 226 - 231, The `recallLessons`
helper in `smart-search` is not forwarding namespace scope, so lesson recall can
return results outside the filtered namespace. Update `recallLessons` to accept
a `namespace` argument and pass it through to the `mem::lesson-recall` call,
then update the `Promise.all` invocation in `smart-search` to supply the same
namespace used by the main search path (from `filterNamespace`/query context) so
lessons stay isolated.
🧹 Nitpick comments (1)
src/functions/remember.ts (1)

80-82: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Namespace supersession guard is intentionally stricter than the project guard.

Unlike the project check above (Line 77, which treats an unscoped legacy memory as a wildcard), this guard uses strict equality, so a namespaced write will never supersede a legacy memory that has no namespace, and vice versa. This matches the "namespace is the stronger boundary" / fail-closed intent, but the divergence from the adjacent project semantics is easy to misread. A one-line clarifying note would help future readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/remember.ts` around lines 80 - 82, Add a brief clarifying
comment in the remember flow near the strict namespace comparison in remember()
to explain that namespace matching is intentionally fail-closed and stricter
than the project guard above. Make it explicit that a namespaced write should
not supersede an unscoped legacy memory, and that this strict equality is by
design to keep namespace as the stronger boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 1355: The README wording is inaccurate because /context, /search,
/smart-search, and /enrich are read-only paths, not mutating endpoints. Update
the sentence in the namespace override section to describe these as endpoints
accepting a namespace override, while keeping /session/start, /observe, and
/remember referenced clearly if needed.

In `@src/functions/claude-bridge.ts`:
- Around line 126-134: The project profile lookup in ClaudeBridge is building
the wrong key because it calls makeProjectProfileKey with only
config.projectPath, so it never matches profiles stored by mem::profile under
the namespaced key. Update the profile fetch in claude-bridge.ts to include the
global namespace from getNamespace() when calling makeProjectProfileKey, keeping
the existing kv.get logic and projectSummary assignment intact. Use the
ClaudeBridgeConfig projectPath check and the mem profile key builder as the main
symbols to locate the fix.

In `@src/functions/context.ts`:
- Around line 40-50: Normalize the namespace before using it in context lookups
so the read key matches the profile write key. In the context-building function
in context.ts, compute a normalized namespace once with
normalizeNamespace(data.namespace) and pass that value to makeProjectProfileKey,
then reuse the same normalized namespace for the lesson/session filters and
header logic instead of raw data.namespace. Also add normalizeNamespace to the
existing import alongside makeProjectProfileKey.

In `@src/functions/enrich.ts`:
- Line 33: The namespace handling in enrich is missing the configured fallback,
so it can end up with an undefined namespace and filter out namespaced memories
in isolated mode. Update the namespace derivation in enrich to match observe by
applying the same getNamespace() fallback before calling normalizeNamespace, and
ensure the resulting namespace is then used consistently in the memory filter
and search path so reads default to the configured namespace.

In `@src/functions/lessons.ts`:
- Around line 121-123: Normalize data.namespace before applying the lessons
filter so it matches the stored namespace form. Update the namespace checks in
the lesson recall and lesson list paths in lessons.ts to compare against
normalizeNamespace(data.namespace) instead of the raw input, using the same
normalization behavior as mem::lesson-save. Make the change in the relevant
lesson filtering logic so callers with whitespace or overlong namespace values
still match the saved lessons.

In `@src/functions/search.ts`:
- Around line 364-375: Remove the dead fail-closed guard in search flow: the
isolated-namespace check in search.ts is unreachable because
isNamespaceScopeIsolated() already implies getNamespace() is set. Delete the
entire if/throw block in the search path so the namespace filter continues to
default to getNamespace() without the unreachable error, and keep the existing
isolation behavior intact.

In `@src/triggers/api.ts`:
- Around line 836-839: The namespace resolution in the filtering paths can fall
back to undefined in isolated mode, causing unfiltered reads instead of failing
closed. Update the logic around filterNamespace in the affected
session/observation/memory handlers to distinguish an explicit wildcardNamespace
from an isolated-mode fallback, and guard the isolated fallback by requiring
getNamespace() to return a resolved namespace before proceeding. If
isNamespaceScopeIsolated() is true and no namespace is configured, return a
closed/error response rather than continuing with undefined filtering.
- Line 295: The REST handlers in src/triggers/api.ts are normalizing malformed
explicit namespace inputs to undefined, which can accidentally fall back to the
default namespace. Update the affected endpoint paths that use
normalizeNamespace(body.namespace) and the related request builders so they
validate “provided but invalid” namespace values at the REST boundary and return
400 instead of dropping them. Keep the whitelisted payload behavior, but only
forward namespace when it is a valid string; otherwise reject the request before
calling the downstream trigger logic.

In `@src/triggers/events.ts`:
- Around line 12-32: Normalize the namespace before creating the Session and
triggering mem::context in src/triggers/events.ts, since the current handler
uses raw data.namespace and can persist unscoped or invalid values. Update the
async event handler to resolve a default via getNamespace when data.namespace is
missing, run it through normalizeNamespace, and use that normalized value both
in the Session object and the sdk.trigger payload. Also add the needed imports
for getNamespace and normalizeNamespace.

In `@test/api-namespace-isolation.test.ts`:
- Around line 163-175: The test for api::session::start only checks the response
payload, so it can miss a regression where the persisted session in KV.sessions
is not stamped with the default namespace. Update the api::session::start test
to also inspect the stored session record after triggering the action, using the
existing sdk and Session flow, and assert that the persisted session’s namespace
matches the configured default namespace (the same value currently expected on
result.body.session.namespace).
- Around line 194-201: The `api::memories` isolation test only verifies the
active namespace filter and misses the wildcard override path. Add a test
alongside the existing `sdk.trigger("api::memories", ...)` coverage that passes
`namespace: "*"` in the request and asserts the memories list is not restricted
to the active namespace, mirroring the session wildcard case. Use the
`api::memories` trigger-based test pattern and the `Memory[]` response shape to
keep the new assertion consistent with the existing suite.

---

Outside diff comments:
In `@src/functions/smart-search.ts`:
- Around line 226-231: The `recallLessons` helper in `smart-search` is not
forwarding namespace scope, so lesson recall can return results outside the
filtered namespace. Update `recallLessons` to accept a `namespace` argument and
pass it through to the `mem::lesson-recall` call, then update the `Promise.all`
invocation in `smart-search` to supply the same namespace used by the main
search path (from `filterNamespace`/query context) so lessons stay isolated.

---

Nitpick comments:
In `@src/functions/remember.ts`:
- Around line 80-82: Add a brief clarifying comment in the remember flow near
the strict namespace comparison in remember() to explain that namespace matching
is intentionally fail-closed and stricter than the project guard above. Make it
explicit that a namespaced write should not supersede an unscoped legacy memory,
and that this strict equality is by design to keep namespace as the stronger
boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46a55749-8631-4386-b747-44e19e918901

📥 Commits

Reviewing files that changed from the base of the PR and between f6f9e3c and e159a48.

📒 Files selected for processing (24)
  • .env.example
  • README.md
  • src/config.ts
  • src/functions/claude-bridge.ts
  • src/functions/compress-synthetic.ts
  • src/functions/compress.ts
  • src/functions/context.ts
  • src/functions/enrich.ts
  • src/functions/export-import.ts
  • src/functions/lessons.ts
  • src/functions/observe.ts
  • src/functions/profile.ts
  • src/functions/remember.ts
  • src/functions/search.ts
  • src/functions/smart-search.ts
  • src/state/memory-utils.ts
  • src/triggers/api.ts
  • src/triggers/events.ts
  • src/types.ts
  • src/utils/namespace.ts
  • test/agent-isolation-search.test.ts
  • test/api-namespace-isolation.test.ts
  • test/cross-project-isolation.test.ts
  • test/namespace-isolation.test.ts

Comment thread README.md Outdated
Comment thread src/functions/claude-bridge.ts
Comment thread src/functions/context.ts
Comment thread src/functions/enrich.ts Outdated
Comment thread src/functions/lessons.ts Outdated
Comment thread src/triggers/api.ts Outdated
Comment thread src/triggers/api.ts Outdated
Comment thread src/triggers/events.ts
Comment thread test/api-namespace-isolation.test.ts
Comment thread test/api-namespace-isolation.test.ts
@scottgl9

Copy link
Copy Markdown
Author

Pushed follow-up commit d787bec to address the review feedback.

Addressed:

  • fixed namespaced project-profile lookup in claude-bridge
  • normalized namespace usage in context, lessons, and event-created sessions
  • defaulted enrich reads to the configured namespace in isolated mode
  • propagated namespace into smart-search lesson recall
  • removed the unreachable namespace guard in search
  • tightened REST namespace validation/fail-closed list filtering in api.ts
  • clarified the strict namespace supersession rule in remember
  • fixed the README wording and extended namespace regression coverage

Validated with:

  • npm test -- test/api-namespace-isolation.test.ts test/namespace-isolation.test.ts
  • npm run build

I left the generic docstring-coverage warning alone since it is repo-wide and not introduced by this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/triggers/api.ts (1)

893-899: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Resolve namespace before listing KV records.

These handlers still perform kv.list(...) before rejecting invalid namespaces or isolated-mode misconfiguration. Move namespace resolution ahead of the full list read so malformed requests fail closed cheaply.

♻️ Proposed adjustment
-      const sessions = await kv.list<Session>(KV.sessions);
       const namespaceResult = resolveListNamespaceFilter(
         parseNamespaceInput(req.query_params?.["namespace"], { allowWildcard: true }),
       );
       if ("status_code" in namespaceResult) {
         return namespaceResult;
       }
       const { filterNamespace } = namespaceResult;
+      const sessions = await kv.list<Session>(KV.sessions);

Apply the same ordering to api::observations and api::memories.

Also applies to: 944-950, 1974-1980

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/triggers/api.ts` around lines 893 - 899, Resolve the namespace before
calling kv.list in the affected handlers so invalid namespaces and isolated-mode
mismatches fail fast without scanning records. Update the api::triggers flow
here, and apply the same ordering in api::observations and api::memories: call
parseNamespaceInput and resolveListNamespaceFilter first, return any status_code
immediately, and only then proceed to the list read using filterNamespace.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/triggers/api.ts`:
- Around line 893-899: Resolve the namespace before calling kv.list in the
affected handlers so invalid namespaces and isolated-mode mismatches fail fast
without scanning records. Update the api::triggers flow here, and apply the same
ordering in api::observations and api::memories: call parseNamespaceInput and
resolveListNamespaceFilter first, return any status_code immediately, and only
then proceed to the list read using filterNamespace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5762b947-1e8b-420f-93d7-952dcb7d5e24

📥 Commits

Reviewing files that changed from the base of the PR and between e159a48 and d787bec.

📒 Files selected for processing (11)
  • README.md
  • src/functions/claude-bridge.ts
  • src/functions/context.ts
  • src/functions/enrich.ts
  • src/functions/lessons.ts
  • src/functions/remember.ts
  • src/functions/search.ts
  • src/functions/smart-search.ts
  • src/triggers/api.ts
  • src/triggers/events.ts
  • test/api-namespace-isolation.test.ts
💤 Files with no reviewable changes (1)
  • src/functions/search.ts
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/functions/claude-bridge.ts
  • src/functions/remember.ts
  • src/functions/lessons.ts
  • src/functions/context.ts
  • src/functions/enrich.ts
  • src/functions/smart-search.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add first-class workspace namespace isolation

1 participant