@contextableai/openclaw-memory-graphiti 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/README.md +424 -0
- package/authorization.ts +202 -0
- package/config.ts +111 -0
- package/docker/.env.example +66 -0
- package/docker/docker-compose.yml +151 -0
- package/graphiti.ts +382 -0
- package/index.ts +942 -0
- package/openclaw.plugin.json +91 -0
- package/package.json +51 -0
- package/schema.zed +23 -0
- package/scripts/dev-setup.sh +146 -0
- package/scripts/dev-start.sh +236 -0
- package/scripts/dev-status.sh +57 -0
- package/scripts/dev-stop.sh +47 -0
- package/search.ts +201 -0
- package/spicedb.ts +174 -0
package/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# @openclaw/memory-graphiti
|
|
2
|
+
|
|
3
|
+
Two-layer memory plugin for OpenClaw: **SpiceDB** for authorization, **Graphiti** for knowledge graph storage.
|
|
4
|
+
|
|
5
|
+
Agents remember conversations as structured entities and facts in a knowledge graph. SpiceDB enforces who can read and write which memories — authorization is at the data layer, not in prompts.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌──────────────────────────────────────────────────┐
|
|
11
|
+
│ OpenClaw Agent │
|
|
12
|
+
│ │
|
|
13
|
+
│ memory_recall ──► SpiceDB ──► Graphiti Search │
|
|
14
|
+
│ memory_store ──► SpiceDB ──► Graphiti Write │
|
|
15
|
+
│ memory_forget ──► SpiceDB ──► Graphiti Delete │
|
|
16
|
+
│ auto-recall ──► SpiceDB ──► Graphiti Search │
|
|
17
|
+
│ auto-capture ──► SpiceDB ──► Graphiti Write │
|
|
18
|
+
└──────────────────────────────────────────────────┘
|
|
19
|
+
│ │
|
|
20
|
+
┌────▼────┐ ┌─────▼─────┐
|
|
21
|
+
│ SpiceDB │ │ Graphiti │
|
|
22
|
+
│ (authz) │ │ MCP Server │
|
|
23
|
+
└─────────┘ └─────┬─────┘
|
|
24
|
+
│
|
|
25
|
+
┌─────▼─────┐
|
|
26
|
+
│ FalkorDB │
|
|
27
|
+
│ (graph) │
|
|
28
|
+
└───────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**SpiceDB** determines which `group_id`s a subject (agent or person) can access, then **Graphiti** searches or stores memories scoped to those groups.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
- [Docker](https://docs.docker.com/get-docker/) and Docker Compose
|
|
38
|
+
- An [OpenAI API key](https://platform.openai.com/api-keys) (Graphiti uses OpenAI for entity extraction and embeddings)
|
|
39
|
+
|
|
40
|
+
### 1. Start Infrastructure
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd docker
|
|
44
|
+
cp .env.example .env
|
|
45
|
+
# Edit .env — set OPENAI_API_KEY at minimum
|
|
46
|
+
docker compose up -d falkordb graphiti-mcp spicedb
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This starts:
|
|
50
|
+
- **FalkorDB** on port 6379 (graph database, web UI on port 3000)
|
|
51
|
+
- **Graphiti MCP Server** on port 8000 (knowledge graph API)
|
|
52
|
+
- **SpiceDB** on port 50051 (authorization engine)
|
|
53
|
+
|
|
54
|
+
### 2. Configure the Plugin
|
|
55
|
+
|
|
56
|
+
Add to your OpenClaw plugin configuration:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"spicedb": {
|
|
61
|
+
"endpoint": "localhost:50051",
|
|
62
|
+
"token": "dev_token",
|
|
63
|
+
"insecure": true
|
|
64
|
+
},
|
|
65
|
+
"graphiti": {
|
|
66
|
+
"endpoint": "http://localhost:8000",
|
|
67
|
+
"defaultGroupId": "main"
|
|
68
|
+
},
|
|
69
|
+
"subjectId": "my-agent"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Initialize SpiceDB Schema
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
openclaw graphiti-mem schema-write
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4. Add Group Membership
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Add the agent to a group
|
|
83
|
+
openclaw graphiti-mem add-member main my-agent --type agent
|
|
84
|
+
|
|
85
|
+
# Add people to groups
|
|
86
|
+
openclaw graphiti-mem add-member family mom --type person
|
|
87
|
+
openclaw graphiti-mem add-member family dad --type person
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Tools
|
|
91
|
+
|
|
92
|
+
The plugin registers four tools available to the agent:
|
|
93
|
+
|
|
94
|
+
### memory_recall
|
|
95
|
+
|
|
96
|
+
Search memories using the knowledge graph. Returns entities and facts the current subject is authorized to see.
|
|
97
|
+
|
|
98
|
+
| Parameter | Type | Default | Description |
|
|
99
|
+
|-----------|------|---------|-------------|
|
|
100
|
+
| `query` | string | *required* | Search query |
|
|
101
|
+
| `limit` | number | 10 | Max results |
|
|
102
|
+
| `scope` | string | `"all"` | `"session"`, `"long-term"`, or `"all"` |
|
|
103
|
+
|
|
104
|
+
Searches both **nodes** (entities) and **facts** (relationships) across all authorized groups in parallel, then deduplicates and ranks by recency.
|
|
105
|
+
|
|
106
|
+
### memory_store
|
|
107
|
+
|
|
108
|
+
Save information to the knowledge graph. Graphiti automatically extracts entities and facts from the content.
|
|
109
|
+
|
|
110
|
+
| Parameter | Type | Default | Description |
|
|
111
|
+
|-----------|------|---------|-------------|
|
|
112
|
+
| `content` | string | *required* | Information to remember |
|
|
113
|
+
| `source_description` | string | `"conversation"` | Context about the source |
|
|
114
|
+
| `involves` | string[] | `[]` | Person/agent IDs involved |
|
|
115
|
+
| `group_id` | string | configured default | Target group for this memory |
|
|
116
|
+
| `longTerm` | boolean | `true` | `false` stores to the current session group |
|
|
117
|
+
|
|
118
|
+
Write authorization is enforced before storing:
|
|
119
|
+
- **Own session groups** auto-create membership (the agent inherits exclusive access)
|
|
120
|
+
- **All other groups** require `contribute` permission in SpiceDB
|
|
121
|
+
|
|
122
|
+
### memory_forget
|
|
123
|
+
|
|
124
|
+
Delete a memory episode. Requires `delete` permission (only the subject who stored the memory can delete it).
|
|
125
|
+
|
|
126
|
+
| Parameter | Type | Description |
|
|
127
|
+
|-----------|------|-------------|
|
|
128
|
+
| `episode_id` | string | Episode UUID to delete |
|
|
129
|
+
|
|
130
|
+
### memory_status
|
|
131
|
+
|
|
132
|
+
Check the health of Graphiti and SpiceDB services. No parameters.
|
|
133
|
+
|
|
134
|
+
## Automatic Behaviors
|
|
135
|
+
|
|
136
|
+
### Auto-Recall
|
|
137
|
+
|
|
138
|
+
When enabled (default: `true`), the plugin searches relevant memories before each agent turn and injects them into the conversation context as `<relevant-memories>` blocks.
|
|
139
|
+
|
|
140
|
+
- Searches up to 5 long-term memories and 3 session memories per turn
|
|
141
|
+
- Deduplicates session results against long-term results
|
|
142
|
+
- Only triggers when the user prompt is at least 5 characters
|
|
143
|
+
|
|
144
|
+
### Auto-Capture
|
|
145
|
+
|
|
146
|
+
When enabled (default: `true`), the plugin captures the last N messages from each completed agent turn and stores them as a batch episode in Graphiti.
|
|
147
|
+
|
|
148
|
+
- Captures up to `maxCaptureMessages` messages (default: 10)
|
|
149
|
+
- Stores to the current session group by default
|
|
150
|
+
- Skips messages shorter than 5 characters and injected context blocks
|
|
151
|
+
- Uses custom extraction instructions for entity/fact extraction
|
|
152
|
+
|
|
153
|
+
## Authorization Model
|
|
154
|
+
|
|
155
|
+
The SpiceDB schema defines four object types:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
person {}
|
|
159
|
+
|
|
160
|
+
agent {
|
|
161
|
+
relation owner: person
|
|
162
|
+
permission act_as = owner
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
group {
|
|
166
|
+
relation member: person | agent
|
|
167
|
+
permission access = member
|
|
168
|
+
permission contribute = member
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
memory_fragment {
|
|
172
|
+
relation source_group: group
|
|
173
|
+
relation involves: person | agent
|
|
174
|
+
relation shared_by: person | agent
|
|
175
|
+
|
|
176
|
+
permission view = involves + shared_by + source_group->access
|
|
177
|
+
permission delete = shared_by
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Groups
|
|
182
|
+
|
|
183
|
+
Groups organize memories and control access. A subject must be a **member** of a group to read (`access`) or write (`contribute`) to it.
|
|
184
|
+
|
|
185
|
+
Membership is managed via the CLI (`graphiti-mem add-member`) or programmatically via `ensureGroupMembership()`.
|
|
186
|
+
|
|
187
|
+
### Memory Fragments
|
|
188
|
+
|
|
189
|
+
Each stored memory creates a `memory_fragment` with three relationships:
|
|
190
|
+
- **source_group** — which group the memory belongs to
|
|
191
|
+
- **shared_by** — who stored the memory (can delete it)
|
|
192
|
+
- **involves** — people/agents mentioned in the memory (can view it)
|
|
193
|
+
|
|
194
|
+
View permission is granted to anyone who is directly involved, shared the memory, or has access to the source group. Delete permission is restricted to the subject who shared (stored) the memory.
|
|
195
|
+
|
|
196
|
+
### Session Groups
|
|
197
|
+
|
|
198
|
+
Session groups (`session-<id>`) provide per-conversation memory isolation:
|
|
199
|
+
- The agent that creates a session automatically gets exclusive membership
|
|
200
|
+
- Other agents cannot read or write to foreign session groups without explicit membership
|
|
201
|
+
- Session memories are searchable within the session scope and are deduplicated against long-term memories
|
|
202
|
+
|
|
203
|
+
## Configuration Reference
|
|
204
|
+
|
|
205
|
+
| Key | Type | Default | Description |
|
|
206
|
+
|-----|------|---------|-------------|
|
|
207
|
+
| `spicedb.endpoint` | string | `localhost:50051` | SpiceDB gRPC endpoint |
|
|
208
|
+
| `spicedb.token` | string | *required* | SpiceDB pre-shared key (supports `${ENV_VAR}`) |
|
|
209
|
+
| `spicedb.insecure` | boolean | `true` | Allow insecure gRPC (for localhost dev) |
|
|
210
|
+
| `graphiti.endpoint` | string | `http://localhost:8000` | Graphiti MCP server URL |
|
|
211
|
+
| `graphiti.defaultGroupId` | string | `main` | Default group for memory storage |
|
|
212
|
+
| `subjectType` | string | `agent` | SpiceDB subject type (`agent` or `person`) |
|
|
213
|
+
| `subjectId` | string | `default` | SpiceDB subject ID (supports `${ENV_VAR}`) |
|
|
214
|
+
| `autoCapture` | boolean | `true` | Auto-capture conversations |
|
|
215
|
+
| `autoRecall` | boolean | `true` | Auto-inject relevant memories |
|
|
216
|
+
| `customInstructions` | string | *(see below)* | Custom extraction instructions for Graphiti |
|
|
217
|
+
| `maxCaptureMessages` | integer | `10` | Max messages per auto-capture batch (1-50) |
|
|
218
|
+
|
|
219
|
+
### Default Custom Instructions
|
|
220
|
+
|
|
221
|
+
When not overridden, the plugin uses these extraction instructions:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
Extract key facts about:
|
|
225
|
+
- Identity: names, roles, titles, contact info
|
|
226
|
+
- Preferences: likes, dislikes, preferred tools/methods
|
|
227
|
+
- Goals: objectives, plans, deadlines
|
|
228
|
+
- Relationships: connections between people, teams, organizations
|
|
229
|
+
- Decisions: choices made, reasoning, outcomes
|
|
230
|
+
- Routines: habits, schedules, recurring patterns
|
|
231
|
+
Do not extract: greetings, filler, meta-commentary about the conversation itself.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Environment Variable Interpolation
|
|
235
|
+
|
|
236
|
+
String values in the config support `${ENV_VAR}` syntax:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"spicedb": {
|
|
241
|
+
"token": "${SPICEDB_TOKEN}"
|
|
242
|
+
},
|
|
243
|
+
"subjectId": "${OPENCLAW_AGENT_ID}"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## CLI Commands
|
|
248
|
+
|
|
249
|
+
All commands are under `graphiti-mem`:
|
|
250
|
+
|
|
251
|
+
| Command | Description |
|
|
252
|
+
|---------|-------------|
|
|
253
|
+
| `graphiti-mem search <query>` | Search memories with authorization. Options: `--limit`, `--scope` |
|
|
254
|
+
| `graphiti-mem episodes` | List recent episodes. Options: `--last`, `--group` |
|
|
255
|
+
| `graphiti-mem status` | Check SpiceDB + Graphiti connectivity |
|
|
256
|
+
| `graphiti-mem schema-write` | Write/update the SpiceDB authorization schema |
|
|
257
|
+
| `graphiti-mem groups` | List authorized groups for the current subject |
|
|
258
|
+
| `graphiti-mem add-member <group-id> <subject-id>` | Add a subject to a group. Options: `--type` |
|
|
259
|
+
| `graphiti-mem import` | Import workspace markdown files into Graphiti. Options: `--workspace`, `--include-sessions`, `--session-dir`, `--group`, `--dry-run` |
|
|
260
|
+
|
|
261
|
+
## Docker Compose
|
|
262
|
+
|
|
263
|
+
The `docker/` directory contains a full-stack Docker Compose configuration:
|
|
264
|
+
|
|
265
|
+
| Service | Port | Description |
|
|
266
|
+
|---------|------|-------------|
|
|
267
|
+
| `falkordb` | 6379, 3000 | Graph database (Redis protocol) + web UI |
|
|
268
|
+
| `graphiti-mcp` | 8000 | Graphiti MCP server (HTTP/SSE) |
|
|
269
|
+
| `postgres` | 5432 | Persistent datastore for SpiceDB |
|
|
270
|
+
| `spicedb-migrate` | — | One-shot: runs SpiceDB DB migrations |
|
|
271
|
+
| `spicedb` | 50051, 8443, 9090 | Authorization engine (gRPC, HTTP, metrics) |
|
|
272
|
+
| `openclaw-gateway` | 18789, 18790 | OpenClaw gateway (optional) |
|
|
273
|
+
|
|
274
|
+
### Infrastructure Only
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
docker compose up -d falkordb graphiti-mcp postgres spicedb-migrate spicedb
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Full Stack (Gateway + Infrastructure)
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
docker compose up -d
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
When running inside Docker Compose, use service hostnames in the plugin config:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"spicedb": {
|
|
291
|
+
"endpoint": "spicedb:50051",
|
|
292
|
+
"token": "${SPICEDB_TOKEN}"
|
|
293
|
+
},
|
|
294
|
+
"graphiti": {
|
|
295
|
+
"endpoint": "http://graphiti-mcp:8000"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## OpenClaw Integration
|
|
301
|
+
|
|
302
|
+
### Selecting the Memory Slot
|
|
303
|
+
|
|
304
|
+
OpenClaw has an exclusive `memory` slot — only one memory plugin is active at a time. To use memory-graphiti, set the slot in your OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"plugins": {
|
|
309
|
+
"slots": {
|
|
310
|
+
"memory": "memory-graphiti"
|
|
311
|
+
},
|
|
312
|
+
"entries": {
|
|
313
|
+
"memory-graphiti": {
|
|
314
|
+
"enabled": true,
|
|
315
|
+
"config": {
|
|
316
|
+
"spicedb": {
|
|
317
|
+
"endpoint": "localhost:50051",
|
|
318
|
+
"token": "dev_token",
|
|
319
|
+
"insecure": true
|
|
320
|
+
},
|
|
321
|
+
"graphiti": {
|
|
322
|
+
"endpoint": "http://localhost:8000",
|
|
323
|
+
"defaultGroupId": "main"
|
|
324
|
+
},
|
|
325
|
+
"subjectType": "agent",
|
|
326
|
+
"subjectId": "my-agent",
|
|
327
|
+
"autoCapture": true,
|
|
328
|
+
"autoRecall": true
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The plugin must be discoverable — either symlinked into `extensions/memory-graphiti` in the OpenClaw installation, or loaded via `plugins.load.paths`.
|
|
337
|
+
|
|
338
|
+
### Initialization
|
|
339
|
+
|
|
340
|
+
After starting infrastructure, write the SpiceDB schema and create group membership:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
openclaw graphiti-mem schema-write
|
|
344
|
+
openclaw graphiti-mem add-member main my-agent --type agent
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The plugin's startup service also auto-creates membership for the configured `subjectId` in the `defaultGroupId`.
|
|
348
|
+
|
|
349
|
+
### Migrating from an Existing Memory Plugin
|
|
350
|
+
|
|
351
|
+
The `import` command migrates workspace markdown files (the universal memory format across all OpenClaw memory plugins) into the Graphiti knowledge graph:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
# Preview what will be imported
|
|
355
|
+
openclaw graphiti-mem import --dry-run
|
|
356
|
+
|
|
357
|
+
# Import workspace files (USER.md, MEMORY.md, memory/*.md, etc.)
|
|
358
|
+
openclaw graphiti-mem import
|
|
359
|
+
|
|
360
|
+
# Also import session transcripts
|
|
361
|
+
openclaw graphiti-mem import --include-sessions
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Workspace files are imported to the configured `defaultGroupId`. Session transcripts are imported to per-session groups (`session-<id>`).
|
|
365
|
+
|
|
366
|
+
### Session Logging
|
|
367
|
+
|
|
368
|
+
OpenClaw's JSONL session logging is always-on core behavior — memory-graphiti does not replace it. The plugin augments session context by:
|
|
369
|
+
|
|
370
|
+
- **Auto-capture**: Extracting entities and facts from conversation turns into the knowledge graph (`agent_end` hook)
|
|
371
|
+
- **Auto-recall**: Injecting relevant memories into the agent's context before each turn (`before_agent_start` hook)
|
|
372
|
+
|
|
373
|
+
The JSONL transcripts remain on disk as a historical record. If you switch back to another memory plugin, they're still available.
|
|
374
|
+
|
|
375
|
+
## Development
|
|
376
|
+
|
|
377
|
+
### Dev Scripts
|
|
378
|
+
|
|
379
|
+
Helper scripts for local development without Docker:
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
scripts/dev-setup.sh # One-time setup: install FalkorDB, Graphiti, SpiceDB
|
|
383
|
+
scripts/dev-start.sh # Start all services with health checks
|
|
384
|
+
scripts/dev-stop.sh # Stop all services
|
|
385
|
+
scripts/dev-status.sh # Check service status
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Running Tests
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
# Unit tests (80 tests, no running services required)
|
|
392
|
+
npm test
|
|
393
|
+
|
|
394
|
+
# E2E tests (13 tests, requires running infrastructure)
|
|
395
|
+
OPENCLAW_LIVE_TEST=1 npm run test:e2e
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Project Structure
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
├── index.ts # Plugin entry: tools, hooks, CLI, service
|
|
402
|
+
├── config.ts # Config schema and validation
|
|
403
|
+
├── graphiti.ts # Graphiti MCP HTTP client (JSON-RPC/SSE)
|
|
404
|
+
├── spicedb.ts # SpiceDB gRPC client wrapper
|
|
405
|
+
├── authorization.ts # Authorization logic (SpiceDB operations)
|
|
406
|
+
├── search.ts # Multi-group parallel search, dedup, formatting
|
|
407
|
+
├── schema.zed # SpiceDB authorization schema
|
|
408
|
+
├── openclaw.plugin.json # Plugin manifest
|
|
409
|
+
├── package.json
|
|
410
|
+
├── docker/
|
|
411
|
+
│ ├── docker-compose.yml # Full infrastructure stack
|
|
412
|
+
│ └── .env.example # Environment variable template
|
|
413
|
+
├── scripts/
|
|
414
|
+
│ ├── dev-setup.sh # One-time dev setup
|
|
415
|
+
│ ├── dev-start.sh # Start dev services
|
|
416
|
+
│ ├── dev-stop.sh # Stop dev services
|
|
417
|
+
│ └── dev-status.sh # Check service status
|
|
418
|
+
├── index.test.ts # Plugin integration tests
|
|
419
|
+
├── authorization.test.ts # Authorization unit tests
|
|
420
|
+
├── search.test.ts # Search unit tests
|
|
421
|
+
├── graphiti.test.ts # Graphiti client tests
|
|
422
|
+
├── config.test.ts # Config parsing tests
|
|
423
|
+
└── e2e.test.ts # End-to-end tests (live services)
|
|
424
|
+
```
|
package/authorization.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization Logic
|
|
3
|
+
*
|
|
4
|
+
* Bridges SpiceDB and Graphiti 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 } 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
|
+
// Authorization Operations
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Look up all group IDs that a subject has access to.
|
|
34
|
+
* Returns group resource IDs from SpiceDB where the subject has the "access" permission.
|
|
35
|
+
*/
|
|
36
|
+
export async function lookupAuthorizedGroups(
|
|
37
|
+
spicedb: SpiceDbClient,
|
|
38
|
+
subject: Subject,
|
|
39
|
+
): Promise<string[]> {
|
|
40
|
+
return spicedb.lookupResources({
|
|
41
|
+
resourceType: "group",
|
|
42
|
+
permission: "access",
|
|
43
|
+
subjectType: subject.type,
|
|
44
|
+
subjectId: subject.id,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Look up all memory fragment IDs that a subject can view.
|
|
50
|
+
* Used for fine-grained post-filtering when needed.
|
|
51
|
+
*/
|
|
52
|
+
export async function lookupViewableFragments(
|
|
53
|
+
spicedb: SpiceDbClient,
|
|
54
|
+
subject: Subject,
|
|
55
|
+
): Promise<string[]> {
|
|
56
|
+
return spicedb.lookupResources({
|
|
57
|
+
resourceType: "memory_fragment",
|
|
58
|
+
permission: "view",
|
|
59
|
+
subjectType: subject.type,
|
|
60
|
+
subjectId: subject.id,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write authorization relationships for a newly stored memory fragment.
|
|
66
|
+
*
|
|
67
|
+
* Creates:
|
|
68
|
+
* - memory_fragment:<id> #source_group group:<groupId>
|
|
69
|
+
* - memory_fragment:<id> #shared_by <sharedBy>
|
|
70
|
+
* - memory_fragment:<id> #involves <person> (for each involved person)
|
|
71
|
+
*/
|
|
72
|
+
export async function writeFragmentRelationships(
|
|
73
|
+
spicedb: SpiceDbClient,
|
|
74
|
+
params: FragmentRelationships,
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const tuples: RelationshipTuple[] = [
|
|
77
|
+
{
|
|
78
|
+
resourceType: "memory_fragment",
|
|
79
|
+
resourceId: params.fragmentId,
|
|
80
|
+
relation: "source_group",
|
|
81
|
+
subjectType: "group",
|
|
82
|
+
subjectId: params.groupId,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
resourceType: "memory_fragment",
|
|
86
|
+
resourceId: params.fragmentId,
|
|
87
|
+
relation: "shared_by",
|
|
88
|
+
subjectType: params.sharedBy.type,
|
|
89
|
+
subjectId: params.sharedBy.id,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
if (params.involves) {
|
|
94
|
+
for (const person of params.involves) {
|
|
95
|
+
tuples.push({
|
|
96
|
+
resourceType: "memory_fragment",
|
|
97
|
+
resourceId: params.fragmentId,
|
|
98
|
+
relation: "involves",
|
|
99
|
+
subjectType: person.type,
|
|
100
|
+
subjectId: person.id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await spicedb.writeRelationships(tuples);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Remove all authorization relationships for a memory fragment.
|
|
110
|
+
* Called when deleting a memory.
|
|
111
|
+
*/
|
|
112
|
+
export async function deleteFragmentRelationships(
|
|
113
|
+
spicedb: SpiceDbClient,
|
|
114
|
+
fragmentId: string,
|
|
115
|
+
params: FragmentRelationships,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const tuples: RelationshipTuple[] = [
|
|
118
|
+
{
|
|
119
|
+
resourceType: "memory_fragment",
|
|
120
|
+
resourceId: fragmentId,
|
|
121
|
+
relation: "source_group",
|
|
122
|
+
subjectType: "group",
|
|
123
|
+
subjectId: params.groupId,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
resourceType: "memory_fragment",
|
|
127
|
+
resourceId: fragmentId,
|
|
128
|
+
relation: "shared_by",
|
|
129
|
+
subjectType: params.sharedBy.type,
|
|
130
|
+
subjectId: params.sharedBy.id,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
if (params.involves) {
|
|
135
|
+
for (const person of params.involves) {
|
|
136
|
+
tuples.push({
|
|
137
|
+
resourceType: "memory_fragment",
|
|
138
|
+
resourceId: fragmentId,
|
|
139
|
+
relation: "involves",
|
|
140
|
+
subjectType: person.type,
|
|
141
|
+
subjectId: person.id,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await spicedb.deleteRelationships(tuples);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a subject has delete permission on a memory fragment.
|
|
151
|
+
*/
|
|
152
|
+
export async function canDeleteFragment(
|
|
153
|
+
spicedb: SpiceDbClient,
|
|
154
|
+
subject: Subject,
|
|
155
|
+
fragmentId: string,
|
|
156
|
+
): Promise<boolean> {
|
|
157
|
+
return spicedb.checkPermission({
|
|
158
|
+
resourceType: "memory_fragment",
|
|
159
|
+
resourceId: fragmentId,
|
|
160
|
+
permission: "delete",
|
|
161
|
+
subjectType: subject.type,
|
|
162
|
+
subjectId: subject.id,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a subject has write (contribute) permission on a group.
|
|
168
|
+
* Used to gate writes to non-session groups — prevents unauthorized memory injection.
|
|
169
|
+
*/
|
|
170
|
+
export async function canWriteToGroup(
|
|
171
|
+
spicedb: SpiceDbClient,
|
|
172
|
+
subject: Subject,
|
|
173
|
+
groupId: string,
|
|
174
|
+
): Promise<boolean> {
|
|
175
|
+
return spicedb.checkPermission({
|
|
176
|
+
resourceType: "group",
|
|
177
|
+
resourceId: groupId,
|
|
178
|
+
permission: "contribute",
|
|
179
|
+
subjectType: subject.type,
|
|
180
|
+
subjectId: subject.id,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Ensure a subject is registered as a member of a group.
|
|
186
|
+
* Idempotent (uses TOUCH operation).
|
|
187
|
+
*/
|
|
188
|
+
export async function ensureGroupMembership(
|
|
189
|
+
spicedb: SpiceDbClient,
|
|
190
|
+
groupId: string,
|
|
191
|
+
member: Subject,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
await spicedb.writeRelationships([
|
|
194
|
+
{
|
|
195
|
+
resourceType: "group",
|
|
196
|
+
resourceId: groupId,
|
|
197
|
+
relation: "member",
|
|
198
|
+
subjectType: member.type,
|
|
199
|
+
subjectId: member.id,
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
}
|