@contextableai/openclaw-memory-rebac 0.1.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 +464 -0
- package/authorization.ts +191 -0
- package/backend.ts +176 -0
- package/backends/backends.json +3 -0
- package/backends/graphiti.defaults.json +8 -0
- package/backends/graphiti.test.ts +292 -0
- package/backends/graphiti.ts +345 -0
- package/backends/registry.ts +36 -0
- package/bin/rebac-mem.ts +144 -0
- package/cli.ts +418 -0
- package/config.ts +141 -0
- package/docker/docker-compose.yml +17 -0
- package/docker/graphiti/Dockerfile +35 -0
- package/docker/graphiti/config_overlay.py +44 -0
- package/docker/graphiti/docker-compose.yml +101 -0
- package/docker/graphiti/graphiti_overlay.py +141 -0
- package/docker/graphiti/startup.py +222 -0
- package/docker/spicedb/docker-compose.yml +79 -0
- package/index.ts +711 -0
- package/openclaw.plugin.json +118 -0
- package/package.json +70 -0
- package/plugin.defaults.json +12 -0
- package/schema.zed +23 -0
- package/search.ts +139 -0
- package/spicedb.ts +355 -0
package/spicedb.ts
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpiceDB Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Wraps @authzed/authzed-node for authorization operations:
|
|
5
|
+
* WriteSchema, WriteRelationships, DeleteRelationships, BulkImportRelationships,
|
|
6
|
+
* LookupResources, CheckPermission.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { v1 } from "@authzed/authzed-node";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export type SpiceDbConfig = {
|
|
16
|
+
endpoint: string;
|
|
17
|
+
token: string;
|
|
18
|
+
insecure: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RelationshipTuple = {
|
|
22
|
+
resourceType: string;
|
|
23
|
+
resourceId: string;
|
|
24
|
+
relation: string;
|
|
25
|
+
subjectType: string;
|
|
26
|
+
subjectId: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ConsistencyMode =
|
|
30
|
+
| { mode: "full" }
|
|
31
|
+
| { mode: "at_least_as_fresh"; token: string }
|
|
32
|
+
| { mode: "minimize_latency" };
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Client
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export class SpiceDbClient {
|
|
39
|
+
private client: ReturnType<typeof v1.NewClient>;
|
|
40
|
+
private promises: ReturnType<typeof v1.NewClient>["promises"];
|
|
41
|
+
|
|
42
|
+
constructor(config: SpiceDbConfig) {
|
|
43
|
+
if (config.insecure) {
|
|
44
|
+
this.client = v1.NewClient(
|
|
45
|
+
config.token,
|
|
46
|
+
config.endpoint,
|
|
47
|
+
v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
this.client = v1.NewClient(config.token, config.endpoint);
|
|
51
|
+
}
|
|
52
|
+
this.promises = this.client.promises;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --------------------------------------------------------------------------
|
|
56
|
+
// Schema
|
|
57
|
+
// --------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
async writeSchema(schema: string): Promise<void> {
|
|
60
|
+
const request = v1.WriteSchemaRequest.create({ schema });
|
|
61
|
+
await this.promises.writeSchema(request);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async readSchema(): Promise<string> {
|
|
65
|
+
const request = v1.ReadSchemaRequest.create({});
|
|
66
|
+
const response = await this.promises.readSchema(request);
|
|
67
|
+
return response.schemaText;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --------------------------------------------------------------------------
|
|
71
|
+
// Relationships
|
|
72
|
+
// --------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
async writeRelationships(tuples: RelationshipTuple[]): Promise<string | undefined> {
|
|
75
|
+
const updates = tuples.map((t) =>
|
|
76
|
+
v1.RelationshipUpdate.create({
|
|
77
|
+
operation: v1.RelationshipUpdate_Operation.TOUCH,
|
|
78
|
+
relationship: v1.Relationship.create({
|
|
79
|
+
resource: v1.ObjectReference.create({
|
|
80
|
+
objectType: t.resourceType,
|
|
81
|
+
objectId: t.resourceId,
|
|
82
|
+
}),
|
|
83
|
+
relation: t.relation,
|
|
84
|
+
subject: v1.SubjectReference.create({
|
|
85
|
+
object: v1.ObjectReference.create({
|
|
86
|
+
objectType: t.subjectType,
|
|
87
|
+
objectId: t.subjectId,
|
|
88
|
+
}),
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const request = v1.WriteRelationshipsRequest.create({ updates });
|
|
95
|
+
const response = await this.promises.writeRelationships(request);
|
|
96
|
+
return response.writtenAt?.token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async deleteRelationships(tuples: RelationshipTuple[]): Promise<void> {
|
|
100
|
+
const updates = tuples.map((t) =>
|
|
101
|
+
v1.RelationshipUpdate.create({
|
|
102
|
+
operation: v1.RelationshipUpdate_Operation.DELETE,
|
|
103
|
+
relationship: v1.Relationship.create({
|
|
104
|
+
resource: v1.ObjectReference.create({
|
|
105
|
+
objectType: t.resourceType,
|
|
106
|
+
objectId: t.resourceId,
|
|
107
|
+
}),
|
|
108
|
+
relation: t.relation,
|
|
109
|
+
subject: v1.SubjectReference.create({
|
|
110
|
+
object: v1.ObjectReference.create({
|
|
111
|
+
objectType: t.subjectType,
|
|
112
|
+
objectId: t.subjectId,
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const request = v1.WriteRelationshipsRequest.create({ updates });
|
|
120
|
+
await this.promises.writeRelationships(request);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async deleteRelationshipsByFilter(params: {
|
|
124
|
+
resourceType: string;
|
|
125
|
+
resourceId: string;
|
|
126
|
+
relation?: string;
|
|
127
|
+
}): Promise<string | undefined> {
|
|
128
|
+
const request = v1.DeleteRelationshipsRequest.create({
|
|
129
|
+
relationshipFilter: v1.RelationshipFilter.create({
|
|
130
|
+
resourceType: params.resourceType,
|
|
131
|
+
optionalResourceId: params.resourceId,
|
|
132
|
+
...(params.relation ? { optionalRelation: params.relation } : {}),
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const response = await this.promises.deleteRelationships(request);
|
|
137
|
+
return response.deletedAt?.token;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --------------------------------------------------------------------------
|
|
141
|
+
// Bulk Import
|
|
142
|
+
// --------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
private toRelationship(t: RelationshipTuple) {
|
|
145
|
+
return v1.Relationship.create({
|
|
146
|
+
resource: v1.ObjectReference.create({
|
|
147
|
+
objectType: t.resourceType,
|
|
148
|
+
objectId: t.resourceId,
|
|
149
|
+
}),
|
|
150
|
+
relation: t.relation,
|
|
151
|
+
subject: v1.SubjectReference.create({
|
|
152
|
+
object: v1.ObjectReference.create({
|
|
153
|
+
objectType: t.subjectType,
|
|
154
|
+
objectId: t.subjectId,
|
|
155
|
+
}),
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Bulk import relationships using the streaming ImportBulkRelationships RPC.
|
|
162
|
+
* More efficient than individual writeRelationships calls for large batches.
|
|
163
|
+
* Falls back to batched writeRelationships if the streaming RPC is unavailable.
|
|
164
|
+
*/
|
|
165
|
+
async bulkImportRelationships(
|
|
166
|
+
tuples: RelationshipTuple[],
|
|
167
|
+
batchSize = 1000,
|
|
168
|
+
): Promise<number> {
|
|
169
|
+
if (tuples.length === 0) return 0;
|
|
170
|
+
|
|
171
|
+
// Try streaming bulk import first
|
|
172
|
+
if (typeof this.promises.bulkImportRelationships === "function") {
|
|
173
|
+
return this.bulkImportViaStream(tuples, batchSize);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Fallback: batched writeRelationships
|
|
177
|
+
return this.bulkImportViaWrite(tuples, batchSize);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private bulkImportViaStream(
|
|
181
|
+
tuples: RelationshipTuple[],
|
|
182
|
+
batchSize: number,
|
|
183
|
+
): Promise<number> {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const stream = this.promises.bulkImportRelationships(
|
|
186
|
+
(err: Error | null, response?: { numLoaded?: string }) => {
|
|
187
|
+
if (err) reject(err);
|
|
188
|
+
else resolve(Number(response?.numLoaded ?? tuples.length));
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
stream.on("error", (err: Error) => {
|
|
193
|
+
reject(err);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < tuples.length; i += batchSize) {
|
|
197
|
+
const chunk = tuples.slice(i, i + batchSize);
|
|
198
|
+
stream.write(
|
|
199
|
+
v1.BulkImportRelationshipsRequest.create({
|
|
200
|
+
relationships: chunk.map((t) => this.toRelationship(t)),
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
stream.end();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async bulkImportViaWrite(
|
|
210
|
+
tuples: RelationshipTuple[],
|
|
211
|
+
batchSize: number,
|
|
212
|
+
): Promise<number> {
|
|
213
|
+
let total = 0;
|
|
214
|
+
for (let i = 0; i < tuples.length; i += batchSize) {
|
|
215
|
+
const chunk = tuples.slice(i, i + batchSize);
|
|
216
|
+
await this.writeRelationships(chunk);
|
|
217
|
+
total += chunk.length;
|
|
218
|
+
}
|
|
219
|
+
return total;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --------------------------------------------------------------------------
|
|
223
|
+
// Read Relationships
|
|
224
|
+
// --------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Read relationships matching a filter. Returns all tuples that match the
|
|
228
|
+
* specified resource type, optional resource ID, optional relation, and
|
|
229
|
+
* optional subject filter. Used by the cleanup command to find which
|
|
230
|
+
* Graphiti episodes have SpiceDB authorization relationships.
|
|
231
|
+
*/
|
|
232
|
+
async readRelationships(params: {
|
|
233
|
+
resourceType: string;
|
|
234
|
+
resourceId?: string;
|
|
235
|
+
relation?: string;
|
|
236
|
+
subjectType?: string;
|
|
237
|
+
subjectId?: string;
|
|
238
|
+
consistency?: ConsistencyMode;
|
|
239
|
+
}): Promise<RelationshipTuple[]> {
|
|
240
|
+
const filterFields: Record<string, unknown> = {
|
|
241
|
+
resourceType: params.resourceType,
|
|
242
|
+
};
|
|
243
|
+
if (params.resourceId) {
|
|
244
|
+
filterFields.optionalResourceId = params.resourceId;
|
|
245
|
+
}
|
|
246
|
+
if (params.relation) {
|
|
247
|
+
filterFields.optionalRelation = params.relation;
|
|
248
|
+
}
|
|
249
|
+
if (params.subjectType) {
|
|
250
|
+
const subjectFilter: Record<string, unknown> = {
|
|
251
|
+
subjectType: params.subjectType,
|
|
252
|
+
};
|
|
253
|
+
if (params.subjectId) {
|
|
254
|
+
subjectFilter.optionalSubjectId = params.subjectId;
|
|
255
|
+
}
|
|
256
|
+
filterFields.optionalSubjectFilter = v1.SubjectFilter.create(subjectFilter);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const request = v1.ReadRelationshipsRequest.create({
|
|
260
|
+
relationshipFilter: v1.RelationshipFilter.create(filterFields),
|
|
261
|
+
consistency: this.buildConsistency(params.consistency),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const results = await this.promises.readRelationships(request);
|
|
265
|
+
const tuples: RelationshipTuple[] = [];
|
|
266
|
+
for (const r of results) {
|
|
267
|
+
const rel = r.relationship;
|
|
268
|
+
if (!rel?.resource || !rel.subject?.object) continue;
|
|
269
|
+
tuples.push({
|
|
270
|
+
resourceType: rel.resource.objectType,
|
|
271
|
+
resourceId: rel.resource.objectId,
|
|
272
|
+
relation: rel.relation,
|
|
273
|
+
subjectType: rel.subject.object.objectType,
|
|
274
|
+
subjectId: rel.subject.object.objectId,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return tuples;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --------------------------------------------------------------------------
|
|
281
|
+
// Permissions
|
|
282
|
+
// --------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
private buildConsistency(mode?: ConsistencyMode) {
|
|
285
|
+
if (!mode || mode.mode === "minimize_latency") {
|
|
286
|
+
return v1.Consistency.create({
|
|
287
|
+
requirement: { oneofKind: "minimizeLatency", minimizeLatency: true },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (mode.mode === "at_least_as_fresh") {
|
|
291
|
+
return v1.Consistency.create({
|
|
292
|
+
requirement: {
|
|
293
|
+
oneofKind: "atLeastAsFresh",
|
|
294
|
+
atLeastAsFresh: v1.ZedToken.create({ token: mode.token }),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return v1.Consistency.create({
|
|
299
|
+
requirement: { oneofKind: "fullyConsistent", fullyConsistent: true },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async checkPermission(params: {
|
|
304
|
+
resourceType: string;
|
|
305
|
+
resourceId: string;
|
|
306
|
+
permission: string;
|
|
307
|
+
subjectType: string;
|
|
308
|
+
subjectId: string;
|
|
309
|
+
consistency?: ConsistencyMode;
|
|
310
|
+
}): Promise<boolean> {
|
|
311
|
+
const request = v1.CheckPermissionRequest.create({
|
|
312
|
+
resource: v1.ObjectReference.create({
|
|
313
|
+
objectType: params.resourceType,
|
|
314
|
+
objectId: params.resourceId,
|
|
315
|
+
}),
|
|
316
|
+
permission: params.permission,
|
|
317
|
+
subject: v1.SubjectReference.create({
|
|
318
|
+
object: v1.ObjectReference.create({
|
|
319
|
+
objectType: params.subjectType,
|
|
320
|
+
objectId: params.subjectId,
|
|
321
|
+
}),
|
|
322
|
+
}),
|
|
323
|
+
consistency: this.buildConsistency(params.consistency),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const response = await this.promises.checkPermission(request);
|
|
327
|
+
return (
|
|
328
|
+
response.permissionship ===
|
|
329
|
+
v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async lookupResources(params: {
|
|
334
|
+
resourceType: string;
|
|
335
|
+
permission: string;
|
|
336
|
+
subjectType: string;
|
|
337
|
+
subjectId: string;
|
|
338
|
+
consistency?: ConsistencyMode;
|
|
339
|
+
}): Promise<string[]> {
|
|
340
|
+
const request = v1.LookupResourcesRequest.create({
|
|
341
|
+
resourceObjectType: params.resourceType,
|
|
342
|
+
permission: params.permission,
|
|
343
|
+
subject: v1.SubjectReference.create({
|
|
344
|
+
object: v1.ObjectReference.create({
|
|
345
|
+
objectType: params.subjectType,
|
|
346
|
+
objectId: params.subjectId,
|
|
347
|
+
}),
|
|
348
|
+
}),
|
|
349
|
+
consistency: this.buildConsistency(params.consistency),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const results = await this.promises.lookupResources(request);
|
|
353
|
+
return results.map((r: { resourceObjectId: string }) => r.resourceObjectId);
|
|
354
|
+
}
|
|
355
|
+
}
|