@contextableai/openclaw-memory-rebac 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,26 @@ This means you can change your storage engine without touching authorization, an
49
49
  - **Docker image**: Custom image (`docker/graphiti/`) with per-component LLM/embedder/reranker configuration, BGE reranker support, and runtime patches for local-model compatibility
50
50
  - **Best for**: Rich entity-relationship extraction, structured knowledge
51
51
 
52
+ ### EverMemOS
53
+
54
+ [EverMemOS](https://github.com/EverMind-AI/EverMemOS) is a memory system with MemCell boundary detection and parallel LLM extraction. It produces richer memory types — episodic memories, user profiles, foresight (predictions), and event logs.
55
+
56
+ - **Storage**: MongoDB + Milvus (vector) + Elasticsearch (keyword)
57
+ - **Transport**: REST API to EverMemOS FastAPI server (port 1995)
58
+ - **Extraction**: MemCell pipeline — automatic boundary detection, then parallel LLM extraction of multiple memory types
59
+ - **Search**: Hybrid (vector + keyword + reranking), configurable via `retrieveMethod`
60
+ - **Docker image**: Built from source (`docker/evermemos/Dockerfile`), pinned to upstream release tag (v1.1.0)
61
+ - **Best for**: Rich memory type diversity, foresight extraction, hybrid search
62
+ - **Limitation**: No `involves` at ingestion — use `memory_share` for post-hoc cross-group sharing
63
+
64
+ #### EverMemOS-specific config
65
+
66
+ | Key | Default | Description |
67
+ |---|---|---|
68
+ | `retrieveMethod` | `"hybrid"` | Search method: `"hybrid"`, `"vector"`, `"keyword"` |
69
+ | `memoryTypes` | `["episodic_memory", "profile", "foresight", "event_log"]` | Which memory types to search |
70
+ | `defaultSenderId` | `"system"` | Sender ID for stored messages |
71
+
52
72
  ## Installation
53
73
 
54
74
  ```bash
@@ -68,22 +88,35 @@ Then restart the gateway. On first start, the plugin automatically:
68
88
  ### Prerequisites
69
89
 
70
90
  - [Docker](https://docs.docker.com/get-docker/) and Docker Compose
71
- - A running LLM endpoint (Graphiti uses an LLM for entity extraction and embeddings)
91
+ - A running LLM endpoint (both backends use LLMs for memory extraction)
72
92
 
73
93
  ### 1. Start Infrastructure
74
94
 
95
+ **Option A: Graphiti backend (default)**
96
+
75
97
  ```bash
76
98
  cd docker
77
99
  cp graphiti/.env.example graphiti/.env
78
100
  # Edit graphiti/.env — set your LLM endpoint and API key
79
- docker compose up -d
101
+ docker compose -f docker-compose.graphiti.yml up -d
80
102
  ```
81
103
 
82
- This starts the full stack:
83
- - **Neo4j** on port 7687 (graph database, browser on port 7474)
84
- - **Graphiti** on port 8000 (FastAPI REST server)
85
- - **PostgreSQL** on port 5432 (persistent datastore for SpiceDB)
86
- - **SpiceDB** on port 50051 (authorization engine)
104
+ This starts: Neo4j (7687), Graphiti (8000), PostgreSQL (5432), SpiceDB (50051).
105
+
106
+ **Option B: EverMemOS backend**
107
+
108
+ ```bash
109
+ cd docker/evermemos
110
+ cp .env.example .env
111
+ # Edit .env — set LLM_API_KEY, VECTORIZE_API_KEY, RERANK_API_KEY
112
+
113
+ cd docker
114
+ docker compose -f docker-compose.evermemos.yml up -d
115
+ ```
116
+
117
+ This starts: EverMemOS all-in-one container (1995), PostgreSQL (5432), SpiceDB (50051). First run builds the image from source (~5 min).
118
+
119
+ Both options share the same SpiceDB instance with the same authorization schema.
87
120
 
88
121
  ### 2. Restart the Gateway
89
122
 
@@ -446,14 +479,18 @@ rebac-mem import --include-sessions
446
479
 
447
480
  ## Docker Compose
448
481
 
449
- The `docker/` directory contains a modular Docker Compose stack. The top-level `docker/docker-compose.yml` includes both sub-stacks:
482
+ The `docker/` directory contains modular Docker Compose stacks. Two top-level compose files select which memory backend to pair with SpiceDB:
450
483
 
451
484
  ```bash
452
- # Start the full stack (Graphiti + SpiceDB)
453
- cd docker
454
- docker compose up -d
485
+ # Graphiti + SpiceDB
486
+ cd docker && docker compose -f docker-compose.graphiti.yml up -d
487
+
488
+ # EverMemOS + SpiceDB
489
+ cd docker && docker compose -f docker-compose.evermemos.yml up -d
455
490
  ```
456
491
 
492
+ Both share the same SpiceDB sub-stack — same authorization schema, same permissions model.
493
+
457
494
  ### Graphiti Stack (`docker/graphiti/`)
458
495
 
459
496
  | Service | Port | Description |
@@ -475,12 +512,31 @@ The custom Docker image extends `zepai/graphiti:latest` with:
475
512
  | `spicedb-migrate` | — | One-shot: runs SpiceDB DB migrations |
476
513
  | `spicedb` | 50051, 8080 | Authorization engine (gRPC, HTTP health) |
477
514
 
515
+ ### EverMemOS Stack (`docker/evermemos/`)
516
+
517
+ Single all-in-one container bundling all required services via supervisord:
518
+
519
+ | Component | Internal Port | Description |
520
+ |-----------|---------------|-------------|
521
+ | MongoDB 7.0 | 27017 | Document store |
522
+ | Elasticsearch 8 | 9200 | Keyword search |
523
+ | Milvus v2.5.2 | 19530 | Vector database (standalone with embedded etcd) |
524
+ | Redis 7 | 6379 | Cache |
525
+ | EverMemOS API | **1995 (exposed)** | FastAPI server (built from source) |
526
+
527
+ The image is built from the [EverMemOS](https://github.com/EverMind-AI/EverMemOS) repository, pinned to release tag `v1.1.0`. Update `EVERMEMOS_VERSION` in `docker/evermemos/docker-compose.yml` to upgrade. Requires ~4 GB RAM.
528
+
529
+ Requires API keys in `docker/evermemos/.env` for LLM, embedding (vectorize), and reranking services. See `.env.example` for all options.
530
+
478
531
  ### Running Stacks Independently
479
532
 
480
533
  ```bash
481
534
  # Graphiti only
482
535
  cd docker/graphiti && docker compose up -d
483
536
 
537
+ # EverMemOS only (without SpiceDB)
538
+ cd docker/evermemos && docker compose up -d
539
+
484
540
  # SpiceDB only
485
541
  cd docker/spicedb && docker compose up -d
486
542
  ```
@@ -511,11 +567,14 @@ OPENCLAW_LIVE_TEST=1 npm run test:e2e
511
567
  ├── openclaw.plugin.json # Plugin manifest
512
568
  ├── package.json
513
569
  ├── backends/
514
- └── graphiti.ts # Graphiti REST backend implementation
570
+ ├── graphiti.ts # Graphiti REST backend implementation
571
+ │ ├── evermemos.ts # EverMemOS REST backend implementation
572
+ │ └── registry.ts # Static backend registry
515
573
  ├── bin/
516
574
  │ └── rebac-mem.ts # Standalone CLI entry point
517
575
  ├── docker/
518
- │ ├── docker-compose.yml # Combined stack (includes both sub-stacks)
576
+ │ ├── docker-compose.graphiti.yml # Graphiti + SpiceDB
577
+ │ ├── docker-compose.evermemos.yml # EverMemOS + SpiceDB
519
578
  │ ├── graphiti/
520
579
  │ │ ├── docker-compose.yml
521
580
  │ │ ├── Dockerfile # Custom Graphiti image with patches
@@ -523,10 +582,17 @@ OPENCLAW_LIVE_TEST=1 npm run test:e2e
523
582
  │ │ ├── graphiti_overlay.py # OpenClawGraphiti class
524
583
  │ │ ├── startup.py # Runtime patches and uvicorn launch
525
584
  │ │ └── .env.example
585
+ │ ├── evermemos/
586
+ │ │ ├── docker-compose.yml # EverMemOS all-in-one container
587
+ │ │ ├── Dockerfile # All-in-one: MongoDB+ES+Milvus+Redis+EverMemOS
588
+ │ │ ├── supervisord.conf # Process manager config
589
+ │ │ └── .env.example # LLM, vectorize, rerank API keys
526
590
  │ └── spicedb/
527
591
  │ └── docker-compose.yml
528
- ├── *.test.ts # Unit tests (96)
529
- ├── e2e.test.ts # End-to-end tests (15, live services)
592
+ ├── *.test.ts # Unit tests (178)
593
+ ├── e2e.test.ts # Graphiti E2E tests (14, live services)
594
+ ├── e2e-backend.test.ts # Backend-agnostic E2E contract (13)
595
+ ├── e2e-evermemos.test.ts # EverMemOS-specific E2E (7)
530
596
  ├── vitest.config.ts # Unit test config
531
597
  └── vitest.e2e.config.ts # E2E test config
532
598
  ```
@@ -66,3 +66,23 @@ export declare function lookupFragmentSourceGroups(spicedb: SpiceDbClient, fragm
66
66
  * Idempotent (uses TOUCH operation).
67
67
  */
68
68
  export declare function ensureGroupMembership(spicedb: SpiceDbClient, groupId: string, member: Subject): Promise<string | undefined>;
69
+ /**
70
+ * Ensure a subject is registered as an owner of a group.
71
+ * Owners have admin permission (can share memories from their groups).
72
+ * Idempotent (uses TOUCH operation).
73
+ */
74
+ export declare function ensureGroupOwnership(spicedb: SpiceDbClient, groupId: string, owner: Subject): Promise<string | undefined>;
75
+ /**
76
+ * Check if a subject has share permission on a memory fragment.
77
+ * Share is granted to: shared_by (storer) + source_group->admin (group owners).
78
+ */
79
+ export declare function canShareFragment(spicedb: SpiceDbClient, subject: Subject, fragmentId: string, zedToken?: string): Promise<boolean>;
80
+ /**
81
+ * Share a memory fragment with one or more subjects by writing `involves` relationships.
82
+ * This grants view permission to the targets (and their agents via involves->represents).
83
+ */
84
+ export declare function shareFragment(spicedb: SpiceDbClient, fragmentId: string, targets: Subject[]): Promise<string | undefined>;
85
+ /**
86
+ * Unshare a memory fragment by removing `involves` relationships for the given targets.
87
+ */
88
+ export declare function unshareFragment(spicedb: SpiceDbClient, fragmentId: string, targets: Subject[]): Promise<void>;
@@ -166,3 +166,63 @@ export async function ensureGroupMembership(spicedb, groupId, member) {
166
166
  },
167
167
  ]);
168
168
  }
169
+ /**
170
+ * Ensure a subject is registered as an owner of a group.
171
+ * Owners have admin permission (can share memories from their groups).
172
+ * Idempotent (uses TOUCH operation).
173
+ */
174
+ export async function ensureGroupOwnership(spicedb, groupId, owner) {
175
+ return spicedb.writeRelationships([
176
+ {
177
+ resourceType: "group",
178
+ resourceId: groupId,
179
+ relation: "owner",
180
+ subjectType: owner.type,
181
+ subjectId: owner.id,
182
+ },
183
+ ]);
184
+ }
185
+ // ============================================================================
186
+ // Share / Unshare
187
+ // ============================================================================
188
+ /**
189
+ * Check if a subject has share permission on a memory fragment.
190
+ * Share is granted to: shared_by (storer) + source_group->admin (group owners).
191
+ */
192
+ export async function canShareFragment(spicedb, subject, fragmentId, zedToken) {
193
+ return spicedb.checkPermission({
194
+ resourceType: "memory_fragment",
195
+ resourceId: fragmentId,
196
+ permission: "share",
197
+ subjectType: subject.type,
198
+ subjectId: subject.id,
199
+ consistency: tokenConsistency(zedToken),
200
+ });
201
+ }
202
+ /**
203
+ * Share a memory fragment with one or more subjects by writing `involves` relationships.
204
+ * This grants view permission to the targets (and their agents via involves->represents).
205
+ */
206
+ export async function shareFragment(spicedb, fragmentId, targets) {
207
+ const tuples = targets.map((target) => ({
208
+ resourceType: "memory_fragment",
209
+ resourceId: fragmentId,
210
+ relation: "involves",
211
+ subjectType: target.type,
212
+ subjectId: target.id,
213
+ }));
214
+ return spicedb.writeRelationships(tuples);
215
+ }
216
+ /**
217
+ * Unshare a memory fragment by removing `involves` relationships for the given targets.
218
+ */
219
+ export async function unshareFragment(spicedb, fragmentId, targets) {
220
+ const tuples = targets.map((target) => ({
221
+ resourceType: "memory_fragment",
222
+ resourceId: fragmentId,
223
+ relation: "involves",
224
+ subjectType: target.type,
225
+ subjectId: target.id,
226
+ }));
227
+ await spicedb.deleteRelationships(tuples);
228
+ }
package/dist/backend.d.ts CHANGED
@@ -31,6 +31,10 @@ export type SearchResult = {
31
31
  * registered in SpiceDB.
32
32
  *
33
33
  * - Graphiti: Resolves once the server has processed the episode (polled in the background).
34
+ * - EverMemOS: Resolves immediately to our generated message_id UUID. This anchor is used
35
+ * for share/involves SpiceDB relationships. Note: search result IDs (MongoDB ObjectIds)
36
+ * differ from this anchor, so involves-based cross-group recall post-filtering won't
37
+ * match — group-level access handles the common path.
34
38
  *
35
39
  * index.ts chains SpiceDB writeFragmentRelationships() to this Promise,
36
40
  * so it always fires at the right time.
@@ -138,6 +142,20 @@ export interface MemoryBackend {
138
142
  * Optional: not all backends separate episodes from fragments.
139
143
  */
140
144
  discoverFragmentIds?(episodeId: string): Promise<string[]>;
145
+ /**
146
+ * Resolve anchor IDs (e.g. message UUIDs written to SpiceDB during store)
147
+ * to actual searchable fragment IDs (e.g. MongoDB ObjectIds).
148
+ *
149
+ * Called during recall when viewable fragment IDs from SpiceDB don't match
150
+ * any search results — indicates the anchors were written before extraction
151
+ * completed (discoverFragmentIds timed out).
152
+ *
153
+ * Returns a Map of anchor → resolved fragment IDs. Only anchors that
154
+ * successfully resolved are included; unresolvable anchors are omitted.
155
+ *
156
+ * Optional: backends where store IDs match search IDs don't need this.
157
+ */
158
+ resolveAnchors?(anchorIds: string[]): Promise<Map<string, string[]>>;
141
159
  /**
142
160
  * Register backend-specific CLI subcommands onto the shared `rebac-mem` command.
143
161
  * Called once during CLI setup. Backend may register any commands it needs.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * EverMemOSBackend — MemoryBackend implementation backed by the EverMemOS FastAPI REST server.
3
+ *
4
+ * EverMemOS communicates via standard HTTP REST endpoints on port 1995.
5
+ * Messages are processed through the MemCell pipeline: boundary detection →
6
+ * parallel LLM extraction of episodic memories, foresight, event logs, and profiles.
7
+ *
8
+ * store() returns immediately with a generated message_id as the fragment anchor.
9
+ * With @timeout_to_background(), store may return 202 Accepted for background processing.
10
+ *
11
+ * discoverFragmentIds() polls a custom trace overlay endpoint to resolve the message_id
12
+ * to actual MongoDB ObjectIds of derived memories. These ObjectIds are what search
13
+ * results return, enabling fragment-level SpiceDB authorization (involves, share).
14
+ *
15
+ * resolveAnchors() provides lazy resolution at recall time: if discoverFragmentIds()
16
+ * timed out during store, unresolved anchors can be resolved later when someone
17
+ * actually tries to recall the involved fragments.
18
+ */
19
+ import type { Command } from "commander";
20
+ import type { MemoryBackend, SearchResult, StoreResult, ConversationTurn, BackendDataset } from "../backend.js";
21
+ export type EverMemOSConfig = {
22
+ endpoint: string;
23
+ defaultGroupId: string;
24
+ requestTimeoutMs: number;
25
+ retrieveMethod: string;
26
+ memoryTypes: string[];
27
+ defaultSenderId: string;
28
+ discoveryPollIntervalMs: number;
29
+ discoveryTimeoutMs: number;
30
+ };
31
+ export declare class EverMemOSBackend implements MemoryBackend {
32
+ private readonly config;
33
+ readonly name = "evermemos";
34
+ private readonly requestTimeoutMs;
35
+ /**
36
+ * Tracks pending stores: messageId → groupId.
37
+ * Populated by store(), consumed by discoverFragmentIds().
38
+ */
39
+ private readonly pendingStores;
40
+ constructor(config: EverMemOSConfig);
41
+ private restCall;
42
+ store(params: {
43
+ content: string;
44
+ groupId: string;
45
+ sourceDescription?: string;
46
+ customPrompt?: string;
47
+ }): Promise<StoreResult>;
48
+ searchGroup(params: {
49
+ query: string;
50
+ groupId: string;
51
+ limit: number;
52
+ sessionId?: string;
53
+ }): Promise<SearchResult[]>;
54
+ enrichSession(params: {
55
+ sessionId: string;
56
+ groupId: string;
57
+ userMsg: string;
58
+ assistantMsg: string;
59
+ }): Promise<void>;
60
+ getConversationHistory(sessionId: string, lastN?: number): Promise<ConversationTurn[]>;
61
+ healthCheck(): Promise<boolean>;
62
+ getStatus(): Promise<Record<string, unknown>>;
63
+ deleteGroup(groupId: string): Promise<void>;
64
+ listGroups(): Promise<BackendDataset[]>;
65
+ deleteFragment(uuid: string, _type?: string): Promise<boolean>;
66
+ /**
67
+ * Poll the trace overlay endpoint to discover MongoDB ObjectIds of derived
68
+ * memories (episodic, foresight, event_log) produced from a stored message.
69
+ *
70
+ * Called by index.ts after store() resolves the fragmentId Promise.
71
+ * Returns ObjectIds that match what search results return, enabling
72
+ * fragment-level SpiceDB authorization (involves, share/unshare).
73
+ */
74
+ discoverFragmentIds(messageId: string): Promise<string[]>;
75
+ /**
76
+ * Lazy resolution: resolve unmatched SpiceDB anchor UUIDs to actual
77
+ * searchable fragment IDs. Called during recall when viewable fragment IDs
78
+ * from SpiceDB don't match any search results.
79
+ */
80
+ resolveAnchors(anchorIds: string[]): Promise<Map<string, string[]>>;
81
+ registerCliCommands(cmd: Command): void;
82
+ }
83
+ export declare const defaults: Record<string, unknown>;
84
+ export declare function create(config: Record<string, unknown>): MemoryBackend;
@@ -0,0 +1,10 @@
1
+ {
2
+ "endpoint": "http://localhost:1995",
3
+ "defaultGroupId": "main",
4
+ "requestTimeoutMs": 30000,
5
+ "retrieveMethod": "hybrid",
6
+ "memoryTypes": ["episodic_memory", "profile", "foresight", "event_log"],
7
+ "defaultSenderId": "system",
8
+ "discoveryPollIntervalMs": 3000,
9
+ "discoveryTimeoutMs": 120000
10
+ }