@contextableai/openclaw-memory-rebac 0.1.0 → 0.1.1
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/dist/authorization.d.ts +57 -0
- package/dist/authorization.js +133 -0
- package/dist/backend.d.ts +135 -0
- package/dist/backend.js +11 -0
- package/dist/backends/graphiti.d.ts +72 -0
- package/dist/backends/graphiti.js +222 -0
- package/dist/backends/registry.d.ts +14 -0
- package/dist/backends/registry.js +12 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.js +446 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +97 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +638 -0
- package/dist/plugin.defaults.json +12 -0
- package/dist/search.d.ts +34 -0
- package/dist/search.js +98 -0
- package/dist/spicedb.d.ts +80 -0
- package/dist/spicedb.js +256 -0
- package/docker/graphiti/.env +50 -0
- package/docker/graphiti/docker-compose.yml +2 -2
- package/docker/graphiti/graphiti_overlay.py +26 -1
- package/docker/graphiti/startup.py +58 -4
- package/docker/spicedb/.env +14 -0
- package/package.json +8 -11
- package/authorization.ts +0 -191
- package/backend.ts +0 -176
- package/backends/backends.json +0 -3
- package/backends/graphiti.test.ts +0 -292
- package/backends/graphiti.ts +0 -345
- package/backends/registry.ts +0 -36
- package/cli.ts +0 -418
- package/config.ts +0 -141
- package/index.ts +0 -711
- package/search.ts +0 -139
- package/spicedb.ts +0 -355
- /package/{backends → dist/backends}/graphiti.defaults.json +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextableai/openclaw-memory-rebac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenClaw two-layer memory plugin: SpiceDB ReBAC authorization + Graphiti knowledge graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,15 +17,9 @@
|
|
|
17
17
|
"knowledge-graph",
|
|
18
18
|
"authorization"
|
|
19
19
|
],
|
|
20
|
+
"main": "./dist/index.js",
|
|
20
21
|
"files": [
|
|
21
|
-
"
|
|
22
|
-
"cli.ts",
|
|
23
|
-
"config.ts",
|
|
24
|
-
"backend.ts",
|
|
25
|
-
"authorization.ts",
|
|
26
|
-
"search.ts",
|
|
27
|
-
"spicedb.ts",
|
|
28
|
-
"backends/",
|
|
22
|
+
"dist",
|
|
29
23
|
"openclaw.plugin.json",
|
|
30
24
|
"plugin.defaults.json",
|
|
31
25
|
"schema.zed",
|
|
@@ -37,8 +31,10 @@
|
|
|
37
31
|
"openclaw": "*"
|
|
38
32
|
},
|
|
39
33
|
"scripts": {
|
|
34
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp plugin.defaults.json dist/ && mkdir -p dist/backends && cp backends/graphiti.defaults.json dist/backends/",
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
40
36
|
"cli": "tsx bin/rebac-mem.ts",
|
|
41
|
-
"typecheck": "tsc
|
|
37
|
+
"typecheck": "tsc 2>&1 | grep -v '../openclaw/' | grep -c 'error TS' | xargs -I{} test {} -eq 0",
|
|
42
38
|
"test": "vitest run",
|
|
43
39
|
"test:watch": "vitest",
|
|
44
40
|
"test:e2e": "OPENCLAW_LIVE_TEST=1 vitest run --config vitest.e2e.config.ts"
|
|
@@ -51,6 +47,7 @@
|
|
|
51
47
|
},
|
|
52
48
|
"devDependencies": {
|
|
53
49
|
"dotenv": "^17.3.1",
|
|
50
|
+
"openclaw": "*",
|
|
54
51
|
"typescript": "^5.9.3",
|
|
55
52
|
"vitest": "^4.0.18"
|
|
56
53
|
},
|
|
@@ -59,7 +56,7 @@
|
|
|
59
56
|
},
|
|
60
57
|
"openclaw": {
|
|
61
58
|
"extensions": [
|
|
62
|
-
"./index.
|
|
59
|
+
"./dist/index.js"
|
|
63
60
|
],
|
|
64
61
|
"install": {
|
|
65
62
|
"npmSpec": "@contextableai/openclaw-memory-rebac",
|
package/authorization.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Authorization Logic
|
|
3
|
-
*
|
|
4
|
-
* Bridges SpiceDB and the memory backend by managing:
|
|
5
|
-
* - Looking up which group_ids a subject can access
|
|
6
|
-
* - Writing fragment authorization relationships when memories are stored
|
|
7
|
-
* - Checking delete permissions
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { SpiceDbClient, RelationshipTuple, ConsistencyMode } from "./spicedb.js";
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// Types
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
export type Subject = {
|
|
17
|
-
type: "agent" | "person";
|
|
18
|
-
id: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type FragmentRelationships = {
|
|
22
|
-
fragmentId: string;
|
|
23
|
-
groupId: string;
|
|
24
|
-
sharedBy: Subject;
|
|
25
|
-
involves?: Subject[];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
// Helpers
|
|
30
|
-
// ============================================================================
|
|
31
|
-
|
|
32
|
-
function tokenConsistency(zedToken?: string): ConsistencyMode | undefined {
|
|
33
|
-
return zedToken ? { mode: "at_least_as_fresh", token: zedToken } : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ============================================================================
|
|
37
|
-
// Authorization Operations
|
|
38
|
-
// ============================================================================
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Look up all group IDs that a subject has access to.
|
|
42
|
-
* Returns group resource IDs from SpiceDB where the subject has the "access" permission.
|
|
43
|
-
*/
|
|
44
|
-
export async function lookupAuthorizedGroups(
|
|
45
|
-
spicedb: SpiceDbClient,
|
|
46
|
-
subject: Subject,
|
|
47
|
-
zedToken?: string,
|
|
48
|
-
): Promise<string[]> {
|
|
49
|
-
return spicedb.lookupResources({
|
|
50
|
-
resourceType: "group",
|
|
51
|
-
permission: "access",
|
|
52
|
-
subjectType: subject.type,
|
|
53
|
-
subjectId: subject.id,
|
|
54
|
-
consistency: tokenConsistency(zedToken),
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Look up all memory fragment IDs that a subject can view.
|
|
60
|
-
* Used for fine-grained post-filtering when needed.
|
|
61
|
-
*/
|
|
62
|
-
export async function lookupViewableFragments(
|
|
63
|
-
spicedb: SpiceDbClient,
|
|
64
|
-
subject: Subject,
|
|
65
|
-
zedToken?: string,
|
|
66
|
-
): Promise<string[]> {
|
|
67
|
-
return spicedb.lookupResources({
|
|
68
|
-
resourceType: "memory_fragment",
|
|
69
|
-
permission: "view",
|
|
70
|
-
subjectType: subject.type,
|
|
71
|
-
subjectId: subject.id,
|
|
72
|
-
consistency: tokenConsistency(zedToken),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Write authorization relationships for a newly stored memory fragment.
|
|
78
|
-
*
|
|
79
|
-
* Creates:
|
|
80
|
-
* - memory_fragment:<id> #source_group group:<groupId>
|
|
81
|
-
* - memory_fragment:<id> #shared_by <sharedBy>
|
|
82
|
-
* - memory_fragment:<id> #involves <person> (for each involved person)
|
|
83
|
-
*/
|
|
84
|
-
export async function writeFragmentRelationships(
|
|
85
|
-
spicedb: SpiceDbClient,
|
|
86
|
-
params: FragmentRelationships,
|
|
87
|
-
): Promise<string | undefined> {
|
|
88
|
-
const tuples: RelationshipTuple[] = [
|
|
89
|
-
{
|
|
90
|
-
resourceType: "memory_fragment",
|
|
91
|
-
resourceId: params.fragmentId,
|
|
92
|
-
relation: "source_group",
|
|
93
|
-
subjectType: "group",
|
|
94
|
-
subjectId: params.groupId,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
resourceType: "memory_fragment",
|
|
98
|
-
resourceId: params.fragmentId,
|
|
99
|
-
relation: "shared_by",
|
|
100
|
-
subjectType: params.sharedBy.type,
|
|
101
|
-
subjectId: params.sharedBy.id,
|
|
102
|
-
},
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
if (params.involves) {
|
|
106
|
-
for (const person of params.involves) {
|
|
107
|
-
tuples.push({
|
|
108
|
-
resourceType: "memory_fragment",
|
|
109
|
-
resourceId: params.fragmentId,
|
|
110
|
-
relation: "involves",
|
|
111
|
-
subjectType: person.type,
|
|
112
|
-
subjectId: person.id,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return spicedb.writeRelationships(tuples);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Remove all authorization relationships for a memory fragment.
|
|
122
|
-
* Uses filter-based deletion — no need to know the group, sharer, or involved parties.
|
|
123
|
-
*/
|
|
124
|
-
export async function deleteFragmentRelationships(
|
|
125
|
-
spicedb: SpiceDbClient,
|
|
126
|
-
fragmentId: string,
|
|
127
|
-
): Promise<string | undefined> {
|
|
128
|
-
return spicedb.deleteRelationshipsByFilter({
|
|
129
|
-
resourceType: "memory_fragment",
|
|
130
|
-
resourceId: fragmentId,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Check if a subject has delete permission on a memory fragment.
|
|
136
|
-
*/
|
|
137
|
-
export async function canDeleteFragment(
|
|
138
|
-
spicedb: SpiceDbClient,
|
|
139
|
-
subject: Subject,
|
|
140
|
-
fragmentId: string,
|
|
141
|
-
zedToken?: string,
|
|
142
|
-
): Promise<boolean> {
|
|
143
|
-
return spicedb.checkPermission({
|
|
144
|
-
resourceType: "memory_fragment",
|
|
145
|
-
resourceId: fragmentId,
|
|
146
|
-
permission: "delete",
|
|
147
|
-
subjectType: subject.type,
|
|
148
|
-
subjectId: subject.id,
|
|
149
|
-
consistency: tokenConsistency(zedToken),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Check if a subject has write (contribute) permission on a group.
|
|
155
|
-
* Used to gate writes to non-session groups — prevents unauthorized memory injection.
|
|
156
|
-
*/
|
|
157
|
-
export async function canWriteToGroup(
|
|
158
|
-
spicedb: SpiceDbClient,
|
|
159
|
-
subject: Subject,
|
|
160
|
-
groupId: string,
|
|
161
|
-
zedToken?: string,
|
|
162
|
-
): Promise<boolean> {
|
|
163
|
-
return spicedb.checkPermission({
|
|
164
|
-
resourceType: "group",
|
|
165
|
-
resourceId: groupId,
|
|
166
|
-
permission: "contribute",
|
|
167
|
-
subjectType: subject.type,
|
|
168
|
-
subjectId: subject.id,
|
|
169
|
-
consistency: tokenConsistency(zedToken),
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Ensure a subject is registered as a member of a group.
|
|
175
|
-
* Idempotent (uses TOUCH operation).
|
|
176
|
-
*/
|
|
177
|
-
export async function ensureGroupMembership(
|
|
178
|
-
spicedb: SpiceDbClient,
|
|
179
|
-
groupId: string,
|
|
180
|
-
member: Subject,
|
|
181
|
-
): Promise<string | undefined> {
|
|
182
|
-
return spicedb.writeRelationships([
|
|
183
|
-
{
|
|
184
|
-
resourceType: "group",
|
|
185
|
-
resourceId: groupId,
|
|
186
|
-
relation: "member",
|
|
187
|
-
subjectType: member.type,
|
|
188
|
-
subjectId: member.id,
|
|
189
|
-
},
|
|
190
|
-
]);
|
|
191
|
-
}
|
package/backend.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MemoryBackend interface
|
|
3
|
-
*
|
|
4
|
-
* All storage-engine specifics live here. The rest of the plugin
|
|
5
|
-
* (SpiceDB auth, search orchestration, tool registration, CLI skeleton)
|
|
6
|
-
* is backend-agnostic and never imports from backends/.
|
|
7
|
-
*
|
|
8
|
-
* Implementing a new backend means satisfying this interface and adding
|
|
9
|
-
* an entry to the factory in config.ts. That's it.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { Command } from "commander";
|
|
13
|
-
|
|
14
|
-
// ============================================================================
|
|
15
|
-
// Shared result types (returned by all backends in a uniform shape)
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* A single memory item returned by searchGroup().
|
|
20
|
-
* Backends are responsible for mapping their native result shape to this.
|
|
21
|
-
*/
|
|
22
|
-
export type SearchResult = {
|
|
23
|
-
/** "node"/"fact" for graph backends; "chunk"/"summary"/"completion" for doc backends */
|
|
24
|
-
type: "node" | "fact" | "chunk" | "summary" | "completion";
|
|
25
|
-
/** Stable ID used by memory_forget. Must be unique within the backend. */
|
|
26
|
-
uuid: string;
|
|
27
|
-
group_id: string;
|
|
28
|
-
summary: string;
|
|
29
|
-
/** Human-readable context hint (entity names, dataset, etc.) */
|
|
30
|
-
context: string;
|
|
31
|
-
created_at: string;
|
|
32
|
-
/** Relevance score [0,1] when available */
|
|
33
|
-
score?: number;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Returned by store(). The fragmentId resolves to the UUID that will be
|
|
38
|
-
* registered in SpiceDB.
|
|
39
|
-
*
|
|
40
|
-
* - Graphiti: Resolves once the server has processed the episode (polled in the background).
|
|
41
|
-
*
|
|
42
|
-
* index.ts chains SpiceDB writeFragmentRelationships() to this Promise,
|
|
43
|
-
* so it always fires at the right time.
|
|
44
|
-
*/
|
|
45
|
-
export type StoreResult = {
|
|
46
|
-
fragmentId: Promise<string>;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A single conversation turn, used for episodic recall.
|
|
51
|
-
* Backends that don't support conversation history return [].
|
|
52
|
-
*/
|
|
53
|
-
export type ConversationTurn = {
|
|
54
|
-
query: string;
|
|
55
|
-
answer: string;
|
|
56
|
-
context?: string;
|
|
57
|
-
created_at?: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Minimal dataset descriptor for the CLI `datasets` command.
|
|
62
|
-
*/
|
|
63
|
-
export type BackendDataset = {
|
|
64
|
-
name: string;
|
|
65
|
-
/** Group ID this dataset maps to (backend-specific derivation) */
|
|
66
|
-
groupId: string;
|
|
67
|
-
/** Optional backend-specific dataset ID */
|
|
68
|
-
id?: string;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// ============================================================================
|
|
72
|
-
// MemoryBackend interface
|
|
73
|
-
// ============================================================================
|
|
74
|
-
|
|
75
|
-
export interface MemoryBackend {
|
|
76
|
-
/** Human-readable backend name for logs and status output */
|
|
77
|
-
readonly name: string;
|
|
78
|
-
|
|
79
|
-
// --------------------------------------------------------------------------
|
|
80
|
-
// Core write
|
|
81
|
-
// Backends MUST return immediately — graph/index construction is async.
|
|
82
|
-
// --------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Ingest content into the group's storage partition.
|
|
86
|
-
* The returned StoreResult.fragmentId resolves when the backend has
|
|
87
|
-
* produced a stable UUID suitable for SpiceDB registration.
|
|
88
|
-
*/
|
|
89
|
-
store(params: {
|
|
90
|
-
content: string;
|
|
91
|
-
groupId: string;
|
|
92
|
-
sourceDescription?: string;
|
|
93
|
-
customPrompt?: string;
|
|
94
|
-
}): Promise<StoreResult>;
|
|
95
|
-
|
|
96
|
-
// --------------------------------------------------------------------------
|
|
97
|
-
// Core read
|
|
98
|
-
// --------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Search within a single group's storage partition.
|
|
102
|
-
* Called in parallel per-group by searchAuthorizedMemories() in search.ts.
|
|
103
|
-
* Backends map their native result shape to SearchResult[].
|
|
104
|
-
*/
|
|
105
|
-
searchGroup(params: {
|
|
106
|
-
query: string;
|
|
107
|
-
groupId: string;
|
|
108
|
-
limit: number;
|
|
109
|
-
sessionId?: string;
|
|
110
|
-
}): Promise<SearchResult[]>;
|
|
111
|
-
|
|
112
|
-
// --------------------------------------------------------------------------
|
|
113
|
-
// Session / episodic memory
|
|
114
|
-
// --------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Optional backend-specific session enrichment, called from agent_end
|
|
118
|
-
* after store() for conversation auto-capture.
|
|
119
|
-
*
|
|
120
|
-
* Graphiti: no-op (addEpisode already handles episodic memory).
|
|
121
|
-
*/
|
|
122
|
-
enrichSession?(params: {
|
|
123
|
-
sessionId: string;
|
|
124
|
-
groupId: string;
|
|
125
|
-
userMsg: string;
|
|
126
|
-
assistantMsg: string;
|
|
127
|
-
}): Promise<void>;
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Retrieve conversation history for a session.
|
|
131
|
-
* Graphiti maps getEpisodes() to this shape.
|
|
132
|
-
* Backends that don't support it return [].
|
|
133
|
-
*/
|
|
134
|
-
getConversationHistory(sessionId: string, lastN?: number): Promise<ConversationTurn[]>;
|
|
135
|
-
|
|
136
|
-
// --------------------------------------------------------------------------
|
|
137
|
-
// Lifecycle
|
|
138
|
-
// --------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
healthCheck(): Promise<boolean>;
|
|
141
|
-
getStatus(): Promise<Record<string, unknown>>;
|
|
142
|
-
|
|
143
|
-
// --------------------------------------------------------------------------
|
|
144
|
-
// Management
|
|
145
|
-
// --------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Delete a group's entire storage partition.
|
|
149
|
-
* Used by `rebac-mem clear-group --confirm`.
|
|
150
|
-
*/
|
|
151
|
-
deleteGroup(groupId: string): Promise<void>;
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* List all storage partitions (datasets/groups) managed by this backend.
|
|
155
|
-
*/
|
|
156
|
-
listGroups(): Promise<BackendDataset[]>;
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Delete a single memory fragment by UUID.
|
|
160
|
-
* Optional: not all backends support sub-dataset deletion.
|
|
161
|
-
* Returns true if deleted, false if the backend doesn't support it.
|
|
162
|
-
*/
|
|
163
|
-
deleteFragment?(uuid: string): Promise<boolean>;
|
|
164
|
-
|
|
165
|
-
// --------------------------------------------------------------------------
|
|
166
|
-
// CLI extension point
|
|
167
|
-
// --------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Register backend-specific CLI subcommands onto the shared `rebac-mem` command.
|
|
171
|
-
* Called once during CLI setup. Backend may register any commands it needs.
|
|
172
|
-
*
|
|
173
|
-
* Example: Graphiti registers episodes, fact, clear-graph
|
|
174
|
-
*/
|
|
175
|
-
registerCliCommands?(cmd: Command): void;
|
|
176
|
-
}
|
package/backends/backends.json
DELETED