@contextableai/openclaw-memory-rebac 0.1.0 → 0.1.2

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/cli.js ADDED
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Shared CLI command registration for rebac-mem.
3
+ *
4
+ * Registers backend-agnostic commands (search, status, schema-write, groups,
5
+ * add-member, import) then calls backend.registerCliCommands() for
6
+ * backend-specific extensions (e.g., graphiti: episodes, fact, clear-graph).
7
+ *
8
+ * Used by both the OpenClaw plugin (index.ts) and the standalone CLI
9
+ * (bin/rebac-mem.ts).
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import { readdir, readFile, stat } from "node:fs/promises";
13
+ import { join, dirname, basename, resolve } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { fileURLToPath } from "node:url";
16
+ import { defaultGroupId } from "./config.js";
17
+ import { lookupAuthorizedGroups, ensureGroupMembership, } from "./authorization.js";
18
+ import { searchAuthorizedMemories } from "./search.js";
19
+ // ============================================================================
20
+ // Session helper (mirrors index.ts — no shared module to avoid circular import)
21
+ // ============================================================================
22
+ function sessionGroupId(sessionId) {
23
+ const sanitized = sessionId.replace(/[^a-zA-Z0-9_-]/g, "-");
24
+ return `session-${sanitized}`;
25
+ }
26
+ // ============================================================================
27
+ // Command Registration
28
+ // ============================================================================
29
+ export function registerCommands(cmd, ctx) {
30
+ const { backend, spicedb, cfg, currentSubject, getLastWriteToken } = ctx;
31
+ const defGroupId = defaultGroupId(cfg);
32
+ // --------------------------------------------------------------------------
33
+ // search
34
+ // --------------------------------------------------------------------------
35
+ cmd
36
+ .command("search")
37
+ .description("Search memories with authorization")
38
+ .argument("<query>", "Search query")
39
+ .option("--limit <n>", "Max results", "10")
40
+ .action(async (query, opts) => {
41
+ const authorizedGroups = await lookupAuthorizedGroups(spicedb, currentSubject, getLastWriteToken());
42
+ if (authorizedGroups.length === 0) {
43
+ console.log("No accessible memory groups.");
44
+ return;
45
+ }
46
+ console.log(`Searching ${authorizedGroups.length} authorized groups...`);
47
+ const results = await searchAuthorizedMemories(backend, {
48
+ query,
49
+ groupIds: authorizedGroups,
50
+ limit: parseInt(opts.limit),
51
+ });
52
+ if (results.length === 0) {
53
+ console.log("No results found.");
54
+ return;
55
+ }
56
+ console.log(JSON.stringify(results, null, 2));
57
+ });
58
+ // --------------------------------------------------------------------------
59
+ // status
60
+ // --------------------------------------------------------------------------
61
+ cmd
62
+ .command("status")
63
+ .description("Check backend + SpiceDB health")
64
+ .action(async () => {
65
+ const backendStatus = await backend.getStatus();
66
+ let spicedbOk = false;
67
+ try {
68
+ await spicedb.readSchema();
69
+ spicedbOk = true;
70
+ }
71
+ catch {
72
+ // unreachable
73
+ }
74
+ console.log(`Backend (${backend.name}): ${backendStatus.healthy ? "OK" : "UNREACHABLE"}`);
75
+ for (const [k, v] of Object.entries(backendStatus)) {
76
+ if (k !== "backend" && k !== "healthy")
77
+ console.log(` ${k}: ${v}`);
78
+ }
79
+ console.log(`SpiceDB: ${spicedbOk ? "OK" : "UNREACHABLE"} (${cfg.spicedb.endpoint})`);
80
+ });
81
+ // --------------------------------------------------------------------------
82
+ // schema-write
83
+ // --------------------------------------------------------------------------
84
+ cmd
85
+ .command("schema-write")
86
+ .description("Write/update SpiceDB authorization schema")
87
+ .action(async () => {
88
+ const schemaPath = join(dirname(fileURLToPath(import.meta.url)), "schema.zed");
89
+ const schema = readFileSync(schemaPath, "utf-8");
90
+ await spicedb.writeSchema(schema);
91
+ console.log("SpiceDB schema written successfully.");
92
+ });
93
+ // --------------------------------------------------------------------------
94
+ // groups
95
+ // --------------------------------------------------------------------------
96
+ cmd
97
+ .command("groups")
98
+ .description("List authorized groups for current subject")
99
+ .action(async () => {
100
+ const groups = await lookupAuthorizedGroups(spicedb, currentSubject, getLastWriteToken());
101
+ if (groups.length === 0) {
102
+ console.log("No authorized groups.");
103
+ return;
104
+ }
105
+ console.log(`Authorized groups for ${currentSubject.type}:${currentSubject.id}:`);
106
+ for (const g of groups)
107
+ console.log(` - ${g}`);
108
+ });
109
+ // --------------------------------------------------------------------------
110
+ // add-member
111
+ // --------------------------------------------------------------------------
112
+ cmd
113
+ .command("add-member")
114
+ .description("Add a subject to a group")
115
+ .argument("<group-id>", "Group ID")
116
+ .argument("<subject-id>", "Subject ID")
117
+ .option("--type <type>", "Subject type (agent|person)", "person")
118
+ .action(async (groupId, subjectId, opts) => {
119
+ const subjectType = opts.type === "agent" ? "agent" : "person";
120
+ await ensureGroupMembership(spicedb, groupId, {
121
+ type: subjectType,
122
+ id: subjectId,
123
+ });
124
+ console.log(`Added ${subjectType}:${subjectId} to group:${groupId}`);
125
+ });
126
+ // --------------------------------------------------------------------------
127
+ // import — ingest workspace files + session transcripts into the backend
128
+ // --------------------------------------------------------------------------
129
+ cmd
130
+ .command("import")
131
+ .description("Import workspace markdown files (and optionally session transcripts) into the backend")
132
+ .option("--workspace <path>", "Workspace directory", join(homedir(), ".openclaw", "workspace"))
133
+ .option("--include-sessions", "Also import session JSONL transcripts", false)
134
+ .option("--sessions-only", "Only import session transcripts (skip workspace files)", false)
135
+ .option("--session-dir <path>", "Session transcripts directory", join(homedir(), ".openclaw", "agents", "main", "sessions"))
136
+ .option("--group <id>", "Target group for workspace files", defGroupId)
137
+ .option("--dry-run", "List files without importing", false)
138
+ .action(async (opts) => {
139
+ const workspacePath = resolve(opts.workspace);
140
+ const targetGroup = opts.group;
141
+ const importSessions = opts.includeSessions || opts.sessionsOnly;
142
+ const importWorkspace = !opts.sessionsOnly;
143
+ let mdFiles = [];
144
+ try {
145
+ const entries = await readdir(workspacePath);
146
+ mdFiles = entries.filter((f) => f.endsWith(".md")).sort();
147
+ }
148
+ catch {
149
+ console.error(`Cannot read workspace directory: ${workspacePath}`);
150
+ return;
151
+ }
152
+ // Also check memory/ subdirectory
153
+ try {
154
+ const memDir = join(workspacePath, "memory");
155
+ const memEntries = await readdir(memDir);
156
+ for (const f of memEntries) {
157
+ if (f.endsWith(".md"))
158
+ mdFiles.push(join("memory", f));
159
+ }
160
+ }
161
+ catch {
162
+ // No memory/ subdirectory — fine
163
+ }
164
+ if (importWorkspace) {
165
+ if (mdFiles.length === 0) {
166
+ console.log("No markdown files found in workspace.");
167
+ }
168
+ else {
169
+ console.log(`Found ${mdFiles.length} workspace file(s) in ${workspacePath}:`);
170
+ for (const f of mdFiles) {
171
+ const filePath = join(workspacePath, f);
172
+ const info = await stat(filePath);
173
+ console.log(` ${f} (${info.size} bytes)`);
174
+ }
175
+ }
176
+ }
177
+ if (opts.dryRun) {
178
+ console.log("\n[dry-run] No files imported.");
179
+ if (importSessions) {
180
+ const sessionPath = resolve(opts.sessionDir);
181
+ try {
182
+ const sessions = (await readdir(sessionPath)).filter((f) => f.endsWith(".jsonl"));
183
+ console.log(`\nFound ${sessions.length} session transcript(s) in ${sessionPath}:`);
184
+ for (const f of sessions) {
185
+ const info = await stat(join(sessionPath, f));
186
+ console.log(` ${f} (${info.size} bytes)`);
187
+ }
188
+ }
189
+ catch {
190
+ console.log(`\nCannot read session directory: ${sessionPath}`);
191
+ }
192
+ }
193
+ return;
194
+ }
195
+ const pending = [];
196
+ const membershipGroups = new Set();
197
+ // Phase 1a: Workspace files
198
+ if (importWorkspace && mdFiles.length > 0) {
199
+ if (importWorkspace)
200
+ membershipGroups.add(targetGroup);
201
+ console.log(`\nPhase 1: Importing workspace files to ${backend.name} (group: ${targetGroup})...`);
202
+ let imported = 0;
203
+ for (const f of mdFiles) {
204
+ const filePath = join(workspacePath, f);
205
+ const content = await readFile(filePath, "utf-8");
206
+ if (!content.trim()) {
207
+ console.log(` Skipping ${f} (empty)`);
208
+ continue;
209
+ }
210
+ try {
211
+ const result = await backend.store({
212
+ content,
213
+ groupId: targetGroup,
214
+ sourceDescription: `Imported from workspace: ${f}`,
215
+ });
216
+ pending.push({ fragmentId: result.fragmentId, groupId: targetGroup, name: f });
217
+ console.log(` Queued ${f} (${content.length} bytes)`);
218
+ imported++;
219
+ }
220
+ catch (err) {
221
+ console.error(` Failed to import ${f}: ${err instanceof Error ? err.message : String(err)}`);
222
+ }
223
+ }
224
+ console.log(`Workspace: ${imported}/${mdFiles.length} files ingested.`);
225
+ }
226
+ // Phase 1b: Session transcripts
227
+ if (importSessions) {
228
+ const sessionPath = resolve(opts.sessionDir);
229
+ let jsonlFiles = [];
230
+ try {
231
+ jsonlFiles = (await readdir(sessionPath)).filter((f) => f.endsWith(".jsonl")).sort();
232
+ }
233
+ catch {
234
+ console.error(`\nCannot read session directory: ${sessionPath}`);
235
+ }
236
+ if (jsonlFiles.length === 0) {
237
+ console.log("\nNo session transcripts found.");
238
+ }
239
+ else {
240
+ console.log(`\nPhase 1: Importing ${jsonlFiles.length} session transcript(s) to ${backend.name}...`);
241
+ let sessionsImported = 0;
242
+ for (const f of jsonlFiles) {
243
+ const sessionId = basename(f, ".jsonl");
244
+ const sessionGroup = sessionGroupId(sessionId);
245
+ const filePath = join(sessionPath, f);
246
+ const raw = await readFile(filePath, "utf-8");
247
+ const lines = raw.split("\n").filter(Boolean);
248
+ const conversationLines = [];
249
+ for (const line of lines) {
250
+ try {
251
+ const entry = JSON.parse(line);
252
+ const msg = (entry.type === "message" && entry.message && typeof entry.message === "object")
253
+ ? entry.message
254
+ : entry;
255
+ const role = msg.role;
256
+ if (role !== "user" && role !== "assistant")
257
+ continue;
258
+ const content = msg.content;
259
+ let text = "";
260
+ if (typeof content === "string") {
261
+ text = content;
262
+ }
263
+ else if (Array.isArray(content)) {
264
+ text = content
265
+ .filter((b) => typeof b === "object" && b !== null &&
266
+ b.type === "text" &&
267
+ typeof b.text === "string")
268
+ .map((b) => b.text)
269
+ .join("\n");
270
+ }
271
+ if (text && text.length >= 5 &&
272
+ !text.includes("<relevant-memories>") &&
273
+ !text.includes("<memory-tools>")) {
274
+ const roleLabel = role === "user" ? "User" : "Assistant";
275
+ conversationLines.push(`${roleLabel}: ${text}`);
276
+ }
277
+ }
278
+ catch {
279
+ // Skip malformed JSONL lines
280
+ }
281
+ }
282
+ if (conversationLines.length === 0) {
283
+ console.log(` Skipping ${f} (no user/assistant messages)`);
284
+ continue;
285
+ }
286
+ try {
287
+ const result = await backend.store({
288
+ content: conversationLines.join("\n"),
289
+ groupId: sessionGroup,
290
+ sourceDescription: `Imported session transcript: ${sessionId}`,
291
+ });
292
+ membershipGroups.add(sessionGroup);
293
+ pending.push({ fragmentId: result.fragmentId, groupId: sessionGroup, name: f });
294
+ console.log(` Queued ${f} (${conversationLines.length} messages) [group: ${sessionGroup}]`);
295
+ sessionsImported++;
296
+ }
297
+ catch (err) {
298
+ console.error(` Failed to import ${f}: ${err instanceof Error ? err.message : String(err)}`);
299
+ }
300
+ }
301
+ console.log(`Sessions: ${sessionsImported}/${jsonlFiles.length} transcripts ingested.`);
302
+ }
303
+ }
304
+ // Phase 1.5: Await all fragmentIds concurrently
305
+ const pendingTuples = [];
306
+ if (pending.length > 0) {
307
+ console.log(`\nResolving ${pending.length} fragment UUIDs...`);
308
+ const resolutions = await Promise.allSettled(pending.map((p) => p.fragmentId));
309
+ for (let i = 0; i < resolutions.length; i++) {
310
+ const r = resolutions[i];
311
+ if (r.status === "fulfilled") {
312
+ const fragmentId = r.value;
313
+ pendingTuples.push({
314
+ resourceType: "memory_fragment",
315
+ resourceId: fragmentId,
316
+ relation: "source_group",
317
+ subjectType: "group",
318
+ subjectId: pending[i].groupId,
319
+ }, {
320
+ resourceType: "memory_fragment",
321
+ resourceId: fragmentId,
322
+ relation: "shared_by",
323
+ subjectType: currentSubject.type,
324
+ subjectId: currentSubject.id,
325
+ });
326
+ console.log(` ${pending[i].name} → ${fragmentId}`);
327
+ }
328
+ else {
329
+ console.warn(` Warning: could not resolve UUID for ${pending[i].name} — SpiceDB linkage skipped`);
330
+ }
331
+ }
332
+ }
333
+ // Phase 2: Bulk write SpiceDB relationships + memberships
334
+ if (pendingTuples.length > 0 || membershipGroups.size > 0) {
335
+ for (const groupId of membershipGroups) {
336
+ pendingTuples.push({
337
+ resourceType: "group",
338
+ resourceId: groupId,
339
+ relation: "member",
340
+ subjectType: currentSubject.type,
341
+ subjectId: currentSubject.id,
342
+ });
343
+ }
344
+ console.log(`\nPhase 2: Writing ${pendingTuples.length} SpiceDB relationships...`);
345
+ try {
346
+ const count = await spicedb.bulkImportRelationships(pendingTuples);
347
+ console.log(`SpiceDB: ${count} relationships written.`);
348
+ }
349
+ catch (err) {
350
+ console.error(`SpiceDB bulk import failed: ${err instanceof Error ? err.message : String(err)}`);
351
+ console.error("Backend episodes were ingested but lack authorization. Re-run import or add relationships manually.");
352
+ }
353
+ }
354
+ console.log("\nImport complete.");
355
+ });
356
+ // --------------------------------------------------------------------------
357
+ // backfill-relationships — retroactively write per-fact SpiceDB relationships
358
+ // --------------------------------------------------------------------------
359
+ if (backend.discoverFragmentIds) {
360
+ cmd
361
+ .command("backfill-relationships")
362
+ .description("Retroactively write per-fact SpiceDB relationships for existing episodes")
363
+ .option("--group <id...>", "Group ID(s) to backfill (default: all authorized groups)")
364
+ .option("--last <n>", "Number of episodes per group to process", "100")
365
+ .option("--dry-run", "Show what would be written without writing", false)
366
+ .action(async (opts) => {
367
+ const lastN = parseInt(opts.last);
368
+ let groups;
369
+ if (opts.group && opts.group.length > 0) {
370
+ groups = opts.group;
371
+ }
372
+ else {
373
+ groups = await lookupAuthorizedGroups(spicedb, currentSubject, getLastWriteToken());
374
+ if (groups.length === 0) {
375
+ console.log("No authorized groups found.");
376
+ return;
377
+ }
378
+ }
379
+ console.log(`Backfilling per-fact SpiceDB relationships for ${groups.length} group(s)...`);
380
+ const tuples = [];
381
+ let totalEpisodes = 0;
382
+ let totalFacts = 0;
383
+ for (const groupId of groups) {
384
+ let episodes;
385
+ try {
386
+ // getEpisodes is Graphiti-specific but we already checked discoverFragmentIds
387
+ episodes = await backend.getEpisodes(groupId, lastN);
388
+ }
389
+ catch {
390
+ console.log(` Skipping group ${groupId} (failed to list episodes)`);
391
+ continue;
392
+ }
393
+ console.log(` Group "${groupId}": ${episodes.length} episode(s)`);
394
+ for (const ep of episodes) {
395
+ const factIds = await backend.discoverFragmentIds(ep.uuid);
396
+ if (factIds.length === 0)
397
+ continue;
398
+ totalEpisodes++;
399
+ totalFacts += factIds.length;
400
+ for (const factId of factIds) {
401
+ tuples.push({
402
+ resourceType: "memory_fragment",
403
+ resourceId: factId,
404
+ relation: "source_group",
405
+ subjectType: "group",
406
+ subjectId: groupId,
407
+ }, {
408
+ resourceType: "memory_fragment",
409
+ resourceId: factId,
410
+ relation: "shared_by",
411
+ subjectType: currentSubject.type,
412
+ subjectId: currentSubject.id,
413
+ });
414
+ }
415
+ if (!opts.dryRun && factIds.length > 0) {
416
+ process.stdout.write(".");
417
+ }
418
+ }
419
+ }
420
+ if (!opts.dryRun && totalFacts > 0) {
421
+ console.log(); // newline after dots
422
+ }
423
+ console.log(`\nDiscovered ${totalFacts} fact(s) across ${totalEpisodes} episode(s).`);
424
+ if (opts.dryRun) {
425
+ console.log(`[dry-run] Would write ${tuples.length} SpiceDB relationships.`);
426
+ return;
427
+ }
428
+ if (tuples.length === 0) {
429
+ console.log("No relationships to write.");
430
+ return;
431
+ }
432
+ console.log(`Writing ${tuples.length} SpiceDB relationships...`);
433
+ try {
434
+ const count = await spicedb.bulkImportRelationships(tuples);
435
+ console.log(`Done: ${count} relationships written.`);
436
+ }
437
+ catch (err) {
438
+ console.error(`SpiceDB bulk import failed: ${err instanceof Error ? err.message : String(err)}`);
439
+ }
440
+ });
441
+ }
442
+ // --------------------------------------------------------------------------
443
+ // Backend-specific extension point
444
+ // --------------------------------------------------------------------------
445
+ backend.registerCliCommands?.(cmd);
446
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Unified configuration for openclaw-memory-rebac.
3
+ *
4
+ * Backend-specific defaults live in backends/<name>.defaults.json.
5
+ * Available backends are listed in backends/backends.json.
6
+ * No backend names appear in this file.
7
+ */
8
+ import type { MemoryBackend } from "./backend.js";
9
+ export type RebacMemoryConfig = {
10
+ backend: string;
11
+ spicedb: {
12
+ endpoint: string;
13
+ token: string;
14
+ insecure: boolean;
15
+ };
16
+ backendConfig: Record<string, unknown>;
17
+ subjectType: "agent" | "person";
18
+ subjectId: string;
19
+ autoCapture: boolean;
20
+ autoRecall: boolean;
21
+ maxCaptureMessages: number;
22
+ };
23
+ export declare const rebacMemoryConfigSchema: {
24
+ parse(value: unknown): RebacMemoryConfig;
25
+ };
26
+ /**
27
+ * Instantiate the configured MemoryBackend from the parsed config.
28
+ * Call this once during plugin registration — the returned backend is stateful.
29
+ */
30
+ export declare function createBackend(cfg: RebacMemoryConfig): MemoryBackend;
31
+ /**
32
+ * Return the default group ID for the active backend.
33
+ */
34
+ export declare function defaultGroupId(cfg: RebacMemoryConfig): string;
package/dist/config.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Unified configuration for openclaw-memory-rebac.
3
+ *
4
+ * Backend-specific defaults live in backends/<name>.defaults.json.
5
+ * Available backends are listed in backends/backends.json.
6
+ * No backend names appear in this file.
7
+ */
8
+ import { backendRegistry } from "./backends/registry.js";
9
+ import pluginDefaults from "./plugin.defaults.json" with { type: "json" };
10
+ // ============================================================================
11
+ // Helpers
12
+ // ============================================================================
13
+ function resolveEnvVars(value) {
14
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
15
+ const envValue = process.env[envVar];
16
+ if (!envValue) {
17
+ throw new Error(`Environment variable ${envVar} is not set`);
18
+ }
19
+ return envValue;
20
+ });
21
+ }
22
+ function assertAllowedKeys(value, allowed, label) {
23
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
24
+ if (unknown.length > 0) {
25
+ throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
26
+ }
27
+ }
28
+ // ============================================================================
29
+ // Config schema
30
+ // ============================================================================
31
+ export const rebacMemoryConfigSchema = {
32
+ parse(value) {
33
+ if (Array.isArray(value)) {
34
+ throw new Error("openclaw-memory-rebac config must be an object, not an array");
35
+ }
36
+ const cfg = (value && typeof value === "object" ? value : {});
37
+ const backendName = typeof cfg.backend === "string" ? cfg.backend : pluginDefaults.backend;
38
+ const entry = backendRegistry[backendName];
39
+ if (!entry)
40
+ throw new Error(`Unknown backend: "${backendName}"`);
41
+ // Top-level allowed keys: shared keys + the backend name key
42
+ assertAllowedKeys(cfg, [
43
+ "backend", "spicedb",
44
+ "subjectType", "subjectId",
45
+ "autoCapture", "autoRecall", "maxCaptureMessages",
46
+ backendName,
47
+ ], "openclaw-memory-rebac config");
48
+ // SpiceDB config (shared)
49
+ const spicedb = cfg.spicedb ?? {};
50
+ assertAllowedKeys(spicedb, ["endpoint", "token", "insecure"], "spicedb config");
51
+ // Backend config: user overrides merged over JSON defaults
52
+ const backendRaw = cfg[backendName] ?? {};
53
+ assertAllowedKeys(backendRaw, Object.keys(entry.defaults), `${backendName} config`);
54
+ const backendConfig = { ...entry.defaults, ...backendRaw };
55
+ const subjectType = cfg.subjectType === "person" ? "person" : pluginDefaults.subjectType;
56
+ const subjectId = typeof cfg.subjectId === "string" ? resolveEnvVars(cfg.subjectId) : pluginDefaults.subjectId;
57
+ return {
58
+ backend: backendName,
59
+ spicedb: {
60
+ endpoint: typeof spicedb.endpoint === "string"
61
+ ? spicedb.endpoint
62
+ : pluginDefaults.spicedb.endpoint,
63
+ token: typeof spicedb.token === "string" ? resolveEnvVars(spicedb.token) : "",
64
+ insecure: typeof spicedb.insecure === "boolean"
65
+ ? spicedb.insecure
66
+ : pluginDefaults.spicedb.insecure,
67
+ },
68
+ backendConfig,
69
+ subjectType,
70
+ subjectId,
71
+ autoCapture: cfg.autoCapture !== false,
72
+ autoRecall: cfg.autoRecall !== false,
73
+ maxCaptureMessages: typeof cfg.maxCaptureMessages === "number" && cfg.maxCaptureMessages > 0
74
+ ? cfg.maxCaptureMessages
75
+ : pluginDefaults.maxCaptureMessages,
76
+ };
77
+ },
78
+ };
79
+ // ============================================================================
80
+ // Backend factory
81
+ // ============================================================================
82
+ /**
83
+ * Instantiate the configured MemoryBackend from the parsed config.
84
+ * Call this once during plugin registration — the returned backend is stateful.
85
+ */
86
+ export function createBackend(cfg) {
87
+ const entry = backendRegistry[cfg.backend];
88
+ if (!entry)
89
+ throw new Error(`Unknown backend: "${cfg.backend}"`);
90
+ return entry.create(cfg.backendConfig);
91
+ }
92
+ /**
93
+ * Return the default group ID for the active backend.
94
+ */
95
+ export function defaultGroupId(cfg) {
96
+ return cfg.backendConfig["defaultGroupId"] ?? "main";
97
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenClaw Memory (ReBAC) Plugin
3
+ *
4
+ * Two-layer memory architecture:
5
+ * - SpiceDB: authorization gateway (who can see what)
6
+ * - MemoryBackend: pluggable storage engine (Graphiti)
7
+ *
8
+ * SpiceDB determines which memories a subject can access.
9
+ * The backend stores the actual knowledge and handles search.
10
+ * Authorization is enforced at the data layer, not in prompts.
11
+ *
12
+ * Backend currently uses Graphiti for knowledge graph storage.
13
+ */
14
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
15
+ declare const rebacMemoryPlugin: {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ kind: "memory";
20
+ configSchema: {
21
+ parse(value: unknown): import("./config.js").RebacMemoryConfig;
22
+ };
23
+ register(api: OpenClawPluginApi): void;
24
+ };
25
+ export default rebacMemoryPlugin;