@pyxmate/memory 0.6.1 → 0.6.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/package.json
CHANGED
|
@@ -13,7 +13,8 @@ description: >
|
|
|
13
13
|
'what did we decide', 'pyx-memory', 'memory', 'MemoryClient',
|
|
14
14
|
'integrate memory', 'memory consolidation', 'multi-tenant',
|
|
15
15
|
'tenant isolation', 'sensitivity', 'encryption', 'confidence',
|
|
16
|
-
'abstention'
|
|
16
|
+
'abstention', 'access control', 'RBAC', 'read-only', 'permissions',
|
|
17
|
+
'agent isolation', 'per-agent memory', 'role-based access'.
|
|
17
18
|
allowed-tools: Read, Grep, Glob, Edit, Write, Bash(curl *)
|
|
18
19
|
argument-hint: "[store|search|ingest] <content or query>"
|
|
19
20
|
---
|
|
@@ -114,6 +115,7 @@ For integrating pyx-memory into TypeScript/Bun projects, see the reference docs:
|
|
|
114
115
|
|---|---|
|
|
115
116
|
| Wire pyx-memory into a consumer project | [patterns/consumer.md](patterns/consumer.md) |
|
|
116
117
|
| Set up embedded memory (full features) | [patterns/embedded.md](patterns/embedded.md) |
|
|
118
|
+
| Multi-tenant, RBAC, sensitivity, encryption | [patterns/access-control.md](patterns/access-control.md) |
|
|
117
119
|
| SDK quick start, package map, DO/DON'T | [reference/sdk-guide.md](reference/sdk-guide.md) |
|
|
118
120
|
| HTTP API endpoints | [reference/http-api.md](reference/http-api.md) |
|
|
119
121
|
| Type signatures and interfaces | [reference/types.md](reference/types.md) |
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# Access Control Patterns
|
|
2
|
+
|
|
3
|
+
Production patterns for restricting who can read and write which memories.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- [Quick Decision Tree](#quick-decision-tree)
|
|
7
|
+
- [Pattern 10: Per-Agent Isolation](#pattern-10-per-agent-isolation)
|
|
8
|
+
- [Pattern 11: Multi-Tenant with Team Scoping](#pattern-11-multi-tenant-with-team-scoping)
|
|
9
|
+
- [Pattern 12: Sensitivity-Based Read Restriction](#pattern-12-sensitivity-based-read-restriction)
|
|
10
|
+
- [Pattern 13: Read-Only vs Read-Write Access](#pattern-13-read-only-vs-read-write-access)
|
|
11
|
+
- [Pattern 14: Full Production Stack](#pattern-14-full-production-stack)
|
|
12
|
+
- [Architecture: Access Policy Layer](#architecture-access-policy-layer)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Quick Decision Tree
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Need to isolate agents from each other?
|
|
20
|
+
→ Use agentId scoping (Pattern 10)
|
|
21
|
+
|
|
22
|
+
Need to isolate organizations/customers?
|
|
23
|
+
→ Use TENANT_MODE=multi + X-Tenant-Id (Pattern 11)
|
|
24
|
+
|
|
25
|
+
Need to hide sensitive data from some users?
|
|
26
|
+
→ Use sensitivity classification + X-Caller-Access-Level (Pattern 12)
|
|
27
|
+
|
|
28
|
+
Need read-only vs read-write roles?
|
|
29
|
+
→ Use API_KEY + ADMIN_API_KEY + an API gateway (Pattern 13)
|
|
30
|
+
|
|
31
|
+
Need all of the above?
|
|
32
|
+
→ Pattern 14 (full production stack)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Pattern 10: Per-Agent Isolation
|
|
38
|
+
|
|
39
|
+
Each agent sees only its own memories. Other agents' memories are invisible — not redacted, just absent from results.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// Agent A — can only see its own memories
|
|
43
|
+
const agentAMemory = new Memory({
|
|
44
|
+
dataDir: './data',
|
|
45
|
+
agentId: 'agent-researcher',
|
|
46
|
+
});
|
|
47
|
+
await agentAMemory.initialize();
|
|
48
|
+
|
|
49
|
+
// Anything agent A stores is tagged with agentId: 'agent-researcher'
|
|
50
|
+
await agentAMemory.store({
|
|
51
|
+
content: 'Found a bug in the auth module',
|
|
52
|
+
type: 'long-term',
|
|
53
|
+
metadata: {},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Agent A's searches only return agent A's memories
|
|
57
|
+
const results = await agentAMemory.search({ query: 'auth bug' });
|
|
58
|
+
// ✓ Returns the entry above
|
|
59
|
+
|
|
60
|
+
// Agent B — completely isolated
|
|
61
|
+
const agentBMemory = new Memory({
|
|
62
|
+
dataDir: './data',
|
|
63
|
+
agentId: 'agent-writer',
|
|
64
|
+
});
|
|
65
|
+
await agentBMemory.initialize();
|
|
66
|
+
|
|
67
|
+
const bResults = await agentBMemory.search({ query: 'auth bug' });
|
|
68
|
+
// ✗ Returns nothing — agent A's memories are invisible to agent B
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Sidecar mode**: Pass `agentId` in the request body or query params — the server filters by it.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Store as agent-researcher
|
|
75
|
+
curl -X POST http://localhost:7822/api/memory/ingest \
|
|
76
|
+
-H "Content-Type: application/json" \
|
|
77
|
+
-d '{"content":"Agent-scoped fact","type":"long-term","metadata":{},"agentId":"agent-researcher"}'
|
|
78
|
+
|
|
79
|
+
# Search as agent-researcher — only sees agent-researcher's memories
|
|
80
|
+
curl 'http://localhost:7822/api/memory/search?query=fact&agentId=agent-researcher'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**When to use**: Multiple AI agents sharing one memory instance, each needing their own knowledge base. Agents that should collaborate can omit `agentId` to share a global pool.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Pattern 11: Multi-Tenant with Team Scoping
|
|
88
|
+
|
|
89
|
+
For SaaS / multi-organization deployments where each customer's data must be completely isolated.
|
|
90
|
+
|
|
91
|
+
### Server config
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# docker-compose.yaml
|
|
95
|
+
environment:
|
|
96
|
+
- TENANT_MODE=multi # enforce X-Tenant-Id on ALL operations
|
|
97
|
+
- API_KEY=your-key # always set API_KEY with multi-tenant
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Per-client setup
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { MemoryClient } from '@pyx-memory/client';
|
|
104
|
+
|
|
105
|
+
// Organization A — engineering team
|
|
106
|
+
const orgAEng = new MemoryClient('http://memory:7822', {
|
|
107
|
+
apiKey: process.env.MEMORY_API_KEY,
|
|
108
|
+
defaultHeaders: {
|
|
109
|
+
'X-Tenant-Id': 'org-acme',
|
|
110
|
+
'X-User-Id': 'alice',
|
|
111
|
+
'X-Team-Id': 'team-engineering',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Organization A — marketing team (same tenant, different team)
|
|
116
|
+
const orgAMkt = new MemoryClient('http://memory:7822', {
|
|
117
|
+
apiKey: process.env.MEMORY_API_KEY,
|
|
118
|
+
defaultHeaders: {
|
|
119
|
+
'X-Tenant-Id': 'org-acme',
|
|
120
|
+
'X-User-Id': 'bob',
|
|
121
|
+
'X-Team-Id': 'team-marketing',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Organization B — completely isolated tenant
|
|
126
|
+
const orgB = new MemoryClient('http://memory:7822', {
|
|
127
|
+
apiKey: process.env.MEMORY_API_KEY,
|
|
128
|
+
defaultHeaders: {
|
|
129
|
+
'X-Tenant-Id': 'org-globex',
|
|
130
|
+
'X-User-Id': 'charlie',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### What's enforced
|
|
136
|
+
|
|
137
|
+
| Scope level | How it works |
|
|
138
|
+
|-------------|-------------|
|
|
139
|
+
| `tenantId` | **Hard isolation.** Tenant A never sees tenant B's data. Enforced server-side when `TENANT_MODE=multi`. |
|
|
140
|
+
| `teamId` | **Soft scoping.** Stored on entries but not enforced by the server. Use application-layer filtering. |
|
|
141
|
+
| `userId` | **Soft scoping.** Stored on entries but not enforced by the server. Use application-layer filtering. |
|
|
142
|
+
| `agentId` | **Hard filtering.** When set in MemoryOptions or query params, only that agent's entries are returned. |
|
|
143
|
+
|
|
144
|
+
**Key rule**: `TENANT_MODE=multi` enforces tenantId. userId and teamId are for your application to filter — pyx-memory stores them but does not enforce boundaries between users/teams within a tenant.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Pattern 12: Sensitivity-Based Read Restriction
|
|
149
|
+
|
|
150
|
+
Control which memories are visible based on the caller's access level. Entries classified above the caller's level are automatically redacted in search results.
|
|
151
|
+
|
|
152
|
+
### How sensitivity classification works
|
|
153
|
+
|
|
154
|
+
Every `store()` automatically scans content and classifies it:
|
|
155
|
+
|
|
156
|
+
| Level | Auto-detected when | Example |
|
|
157
|
+
|-------|-------------------|---------|
|
|
158
|
+
| `public` | No credentials or PII detected | "Prefer tabs over spaces" |
|
|
159
|
+
| `internal` | PII found (email, phone, SSN, etc.) | "Alice's email is alice@acme.com" |
|
|
160
|
+
| `secret` | Credentials found (API keys, tokens, passwords) | "The API key is sk-ant-..." |
|
|
161
|
+
|
|
162
|
+
### Restricting search results
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Embedded: use maxSensitivity search param
|
|
166
|
+
const results = await memory.search({
|
|
167
|
+
query: 'config',
|
|
168
|
+
maxSensitivity: 'internal', // only public + internal; secret entries are redacted
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Sidecar: use X-Caller-Access-Level header
|
|
172
|
+
const restricted = new MemoryClient('http://memory:7822', {
|
|
173
|
+
apiKey: process.env.MEMORY_API_KEY,
|
|
174
|
+
defaultHeaders: {
|
|
175
|
+
'X-Caller-Access-Level': 'internal', // this client never sees secret entries
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### What "redacted" means
|
|
181
|
+
|
|
182
|
+
When a search returns entries above the caller's access level, the content is replaced with `[REDACTED: sensitive content]`. The entry metadata (id, type, createdAt) is still visible so the caller knows something exists — they just can't read it.
|
|
183
|
+
|
|
184
|
+
### Sensitivity policy options
|
|
185
|
+
|
|
186
|
+
| `SENSITIVITY_POLICY` | Behavior |
|
|
187
|
+
|----------------------|----------|
|
|
188
|
+
| `flag` (default) | Classify and store sensitivity level. No content modification. |
|
|
189
|
+
| `redact` | Replace detected credentials with `[REDACTED]` before storage. |
|
|
190
|
+
| `block` | Reject any ingest containing credentials (HTTP 400). |
|
|
191
|
+
| `encrypt` | Encrypt `secret` entries at rest with AES-256-GCM. Transparent decrypt on read when key is available. |
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Production config with encryption
|
|
195
|
+
environment:
|
|
196
|
+
- SENSITIVITY_POLICY=encrypt
|
|
197
|
+
- ENCRYPTION_KEY=your-64-hex-char-key # 32 bytes as hex
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Pattern 13: Read-Only vs Read-Write Access
|
|
203
|
+
|
|
204
|
+
pyx-memory has two auth tiers: `API_KEY` (read + write) and `ADMIN_API_KEY` (destructive ops). For finer read-only vs read-write control, use an API gateway or proxy layer in front.
|
|
205
|
+
|
|
206
|
+
### Built-in auth tiers
|
|
207
|
+
|
|
208
|
+
| Key | Allowed operations |
|
|
209
|
+
|-----|-------------------|
|
|
210
|
+
| `API_KEY` | All read operations (search, get, list, stats) + write operations (store, ingestFile) |
|
|
211
|
+
| `ADMIN_API_KEY` | Destructive operations: DELETE, forget, decay, consolidate, reindex, deleteBySource |
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Give agents API_KEY (read + write), give admin tools ADMIN_API_KEY
|
|
215
|
+
environment:
|
|
216
|
+
- API_KEY=agent-key-for-read-write
|
|
217
|
+
- ADMIN_API_KEY=admin-key-for-destructive-ops
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Adding read-only access via a proxy
|
|
221
|
+
|
|
222
|
+
pyx-memory doesn't have a native "read-only API key." To implement read-only roles, put a reverse proxy (nginx, Caddy, API gateway) in front:
|
|
223
|
+
|
|
224
|
+
```nginx
|
|
225
|
+
# nginx example: read-only role blocks POST/PUT/DELETE
|
|
226
|
+
location /api/memory/ {
|
|
227
|
+
# Read-only key can only GET
|
|
228
|
+
if ($http_authorization = "Bearer readonly-key") {
|
|
229
|
+
limit_except GET {
|
|
230
|
+
deny all;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
proxy_pass http://memory:7822;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Or implement it in your application layer:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// Application-level access control middleware
|
|
241
|
+
function memoryAccessMiddleware(req: Request, userRole: string): boolean {
|
|
242
|
+
const method = req.method;
|
|
243
|
+
const isWrite = method === 'POST' || method === 'PUT' || method === 'DELETE';
|
|
244
|
+
|
|
245
|
+
if (userRole === 'viewer' && isWrite) {
|
|
246
|
+
throw new ForbiddenError('Read-only access — cannot modify memory');
|
|
247
|
+
}
|
|
248
|
+
if (userRole === 'editor' && req.url.includes('/consolidate')) {
|
|
249
|
+
throw new ForbiddenError('Editors cannot run consolidation');
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Pattern 14: Full Production Stack
|
|
258
|
+
|
|
259
|
+
Combines all patterns for a production multi-tenant deployment with sensitivity, encryption, and role-based access.
|
|
260
|
+
|
|
261
|
+
### Server config
|
|
262
|
+
|
|
263
|
+
```yaml
|
|
264
|
+
# docker-compose.yaml
|
|
265
|
+
services:
|
|
266
|
+
memory:
|
|
267
|
+
image: ghcr.io/pyx-corp/pyx-memory-v1:latest
|
|
268
|
+
environment:
|
|
269
|
+
- DATA_DIR=/data
|
|
270
|
+
- API_KEY=${MEMORY_API_KEY}
|
|
271
|
+
- ADMIN_API_KEY=${MEMORY_ADMIN_KEY}
|
|
272
|
+
- TENANT_MODE=multi
|
|
273
|
+
- SENSITIVITY_POLICY=encrypt
|
|
274
|
+
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
|
275
|
+
- PII_POLICY=flag
|
|
276
|
+
- RATE_LIMIT_RPM=120
|
|
277
|
+
- CORS_ORIGIN=https://app.example.com
|
|
278
|
+
- NODE_ENV=production
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Client setup per role
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { MemoryClient } from '@pyx-memory/client';
|
|
285
|
+
|
|
286
|
+
const MEMORY_URL = 'http://memory:7822';
|
|
287
|
+
|
|
288
|
+
// Admin client — full access, destructive ops allowed
|
|
289
|
+
function createAdminClient(tenantId: string) {
|
|
290
|
+
return new MemoryClient(MEMORY_URL, {
|
|
291
|
+
apiKey: process.env.MEMORY_ADMIN_KEY,
|
|
292
|
+
defaultHeaders: {
|
|
293
|
+
'X-Tenant-Id': tenantId,
|
|
294
|
+
'X-Caller-Access-Level': 'secret',
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Agent client — read + write, scoped to agent + tenant
|
|
300
|
+
function createAgentClient(tenantId: string, agentId: string) {
|
|
301
|
+
return new MemoryClient(MEMORY_URL, {
|
|
302
|
+
apiKey: process.env.MEMORY_API_KEY,
|
|
303
|
+
defaultHeaders: {
|
|
304
|
+
'X-Tenant-Id': tenantId,
|
|
305
|
+
'X-Caller-Access-Level': 'internal', // can't see secret entries
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
// Note: agentId is passed per-request, not via headers
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Dashboard client — read-only (enforce at application layer)
|
|
312
|
+
function createDashboardClient(tenantId: string, userId: string) {
|
|
313
|
+
return new MemoryClient(MEMORY_URL, {
|
|
314
|
+
apiKey: process.env.MEMORY_API_KEY,
|
|
315
|
+
defaultHeaders: {
|
|
316
|
+
'X-Tenant-Id': tenantId,
|
|
317
|
+
'X-User-Id': userId,
|
|
318
|
+
'X-Caller-Access-Level': 'public', // most restricted view
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### What each role sees
|
|
325
|
+
|
|
326
|
+
| Role | tenantId | agentId | maxSensitivity | Write | Delete/Admin |
|
|
327
|
+
|------|----------|---------|----------------|-------|-------------|
|
|
328
|
+
| Admin | scoped | all | secret | yes | yes (ADMIN_API_KEY) |
|
|
329
|
+
| Agent | scoped | scoped | internal | yes | no |
|
|
330
|
+
| Dashboard | scoped | all | public | no (app-enforced) | no |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Architecture: Access Policy Layer
|
|
335
|
+
|
|
336
|
+
For organizations that need true RBAC (role-based access control) with fine-grained per-entry permissions, the recommended architecture adds a policy layer between your application and pyx-memory:
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
User/Agent Request
|
|
340
|
+
↓
|
|
341
|
+
[Your Application]
|
|
342
|
+
↓
|
|
343
|
+
[Access Policy Layer] ← checks role + scope before forwarding
|
|
344
|
+
↓
|
|
345
|
+
[pyx-memory] ← stores data, unaware of permissions
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Why this is better than building ACL into the memory store
|
|
349
|
+
|
|
350
|
+
1. **Separation of concerns** — memory stays fast and simple (store/retrieve). Access logic lives in your application where business rules belong.
|
|
351
|
+
2. **Flexibility** — you can change access rules without migrating data or changing the memory schema.
|
|
352
|
+
3. **Auditability** — policy decisions are logged at the application layer, not buried in storage internals.
|
|
353
|
+
|
|
354
|
+
### Implementation approach
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Define scopes as metadata on memory entries
|
|
358
|
+
await memory.store({
|
|
359
|
+
content: 'Q4 revenue projections',
|
|
360
|
+
type: 'long-term',
|
|
361
|
+
metadata: {
|
|
362
|
+
scope: 'finance:confidential', // your custom scope tag
|
|
363
|
+
allowedTeams: ['finance', 'exec'], // who can read
|
|
364
|
+
allowedRoles: ['analyst', 'admin'], // which roles
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// In your API gateway, filter results based on caller identity
|
|
369
|
+
function filterByAccess(entries: MemoryEntry[], caller: CallerIdentity): MemoryEntry[] {
|
|
370
|
+
return entries.filter(entry => {
|
|
371
|
+
const meta = entry.metadata;
|
|
372
|
+
if (!meta.allowedTeams) return true; // no restriction = public
|
|
373
|
+
return (
|
|
374
|
+
meta.allowedTeams.includes(caller.teamId) ||
|
|
375
|
+
meta.allowedRoles.includes(caller.role)
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
This approach is not built into pyx-memory because access policies are inherently application-specific. The memory store provides the building blocks (tenant isolation, sensitivity classification, metadata storage) — your application composes them into the access model that fits your organization.
|