@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -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.