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