@pyxmate/memory 0.15.2 → 0.16.0

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/index.d.ts CHANGED
@@ -337,6 +337,15 @@ interface MemoryEntry {
337
337
  userId?: string;
338
338
  /** Team/group ID within the tenant. */
339
339
  teamId?: string;
340
+ /**
341
+ * Namespace ID — the primary protected resource for ReBAC authorization.
342
+ * `undefined` (NULL on the row) means the entry lives in the legacy
343
+ * "tenant-root" bucket and is visible to anyone with tenant access (the
344
+ * pre-ReBAC posture). Setting `namespaceId` opts the entry into AuthzPlan-
345
+ * based filtering, where visibility is computed from `authz_tuples` for
346
+ * the calling principal.
347
+ */
348
+ namespaceId?: string;
340
349
  /**
341
350
  * When true, consolidation leaves this entry alone — it is never archived by
342
351
  * decay and never merged (nor merged-into) by semantic deduplication. Use for
@@ -365,6 +374,22 @@ interface MemorySearchParams {
365
374
  userId?: string;
366
375
  /** Team/group ID within the tenant. */
367
376
  teamId?: string;
377
+ /**
378
+ * AuthzPlan-derived visibility list. Populated internally by `Memory.search`
379
+ * after computing the plan from the calling principal — callers should
380
+ * NOT set this directly. Empty array means "no granted namespaces" (only
381
+ * legacy NULL-namespace entries are visible). `undefined` skips the
382
+ * filter (single-tenant / pre-ReBAC compat).
383
+ *
384
+ * "Search within this specific folder" UX is intentionally NOT supported
385
+ * via a singular `namespaceId` field for v1 — adding it would force
386
+ * intersection logic with the visibility list (and confused-deputy risk
387
+ * if the singular value isn't validated against the plan). Callers that
388
+ * need it can compute the singleton intersection client-side and pass
389
+ * `namespaceIds: [chosenNamespace]` after verifying access via the
390
+ * admin API.
391
+ */
392
+ namespaceIds?: string[];
368
393
  }
369
394
  interface MemorySearchResult {
370
395
  entries: MemoryEntry[];
@@ -474,6 +499,8 @@ interface MemoryIngestRequest {
474
499
  userId?: string;
475
500
  /** Team/group ID within the tenant. */
476
501
  teamId?: string;
502
+ /** Namespace ID — see `MemoryEntry.namespaceId`. */
503
+ namespaceId?: string;
477
504
  /**
478
505
  * Exclude this entry from consolidation (decay-archive and semantic dedup).
479
506
  * See `MemoryEntry.pinned` for full semantics.
@@ -629,4 +656,51 @@ interface IngestErrorEvent {
629
656
  }
630
657
  type IngestEvent = IngestProgressEvent | IngestHeartbeatEvent | IngestResultEvent | IngestErrorEvent;
631
658
 
632
- export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type EnrichmentCallbacks, type ExtendedMemoryInterface, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestErrorEvent, type IngestEvent, type IngestFileOptions, type IngestHeartbeatEvent, type IngestProgressEvent, type IngestRelationship, type IngestResultEvent, type IngestStage, type IngestionResult, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, RAGStrategy, SensitivityLevel, type StoreInput, StoreTarget, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, VectorProvider };
659
+ /**
660
+ * Caller identity for ReBAC authorization.
661
+ *
662
+ * The canonical "subject" in Zanzibar terms — passed alongside requests so the
663
+ * memory layer can compute an AuthzPlan (visible namespaces + entry overrides)
664
+ * before any retrieval source fans out.
665
+ *
666
+ * Identity comes from authenticated request context (X-Tenant-Id / auth token
667
+ * claims), never from request bodies or multipart form fields. The legacy
668
+ * `userId` / `teamId` / `agentId` columns on MemoryEntry remain for audit and
669
+ * legacy filters but MUST NOT drive authorization decisions — they are
670
+ * collision-prone aliases (a service principal sharing an ID with a human
671
+ * user has no protection against confused-deputy attacks).
672
+ *
673
+ * Sensitivity / clearance is intentionally NOT on this object. It is a
674
+ * MAC-style classification, orthogonal to RBAC, and continues to be carried
675
+ * via the `X-Caller-Access-Level` header → `MemorySearchParams.maxSensitivity`.
676
+ * Conflating the two couples future changes (e.g. per-namespace classification
677
+ * rules) to identity propagation.
678
+ */
679
+ interface PrincipalContext {
680
+ /**
681
+ * Hard isolation boundary. Required even in single-tenant deployments —
682
+ * single-mode passes a stable sentinel (`SINGLE_TENANT_ID`) so authz code
683
+ * paths look identical regardless of mode.
684
+ */
685
+ tenantId: string;
686
+ /**
687
+ * Stable subject ID (within the tenant). For humans this is the userId;
688
+ * for AI runtimes it is the agentId; for system actors it is a service
689
+ * identifier. Combined with `kind`, forms the Zanzibar subject coordinate
690
+ * `<kind>:<principalId>`.
691
+ */
692
+ principalId: string;
693
+ /**
694
+ * Subject namespace. Distinguishes humans from AI agents from internal
695
+ * services so a userId/agentId/serviceId collision cannot grant
696
+ * unintended access.
697
+ * - `user`: human end user
698
+ * - `agent`: AI runtime acting on a user's behalf or autonomously
699
+ * - `service`: non-AI internal system (cron, ETL, admin tooling)
700
+ */
701
+ kind: 'user' | 'agent' | 'service';
702
+ }
703
+ /** Sentinel tenant ID used in single-tenant deployments. */
704
+ declare const SINGLE_TENANT_ID = "_single";
705
+
706
+ export { type AgentId, type ApiResponse, type ConsolidationRunResult, DEFAULTS, EmbeddingProviderName, type EnrichmentCallbacks, type ExtendedMemoryInterface, type GraphFailureMode, type GraphNode, type GraphRelationship, type GraphTraversalResult, type IngestEntity, type IngestErrorEvent, type IngestEvent, type IngestFileOptions, type IngestHeartbeatEvent, type IngestProgressEvent, type IngestRelationship, type IngestResultEvent, type IngestStage, type IngestionResult, MemoryClient, type MemoryClientOptions, type MemoryEntry, type MemoryIngestRequest, type MemoryInterface, type MemoryListParams, type MemoryListResult, type MemorySearchParams, type MemorySearchResult, MemoryServerError, type MemoryStats, MemoryType, type PrincipalContext, RAGStrategy, SINGLE_TENANT_ID, SensitivityLevel, type StoreInput, StoreTarget, type TemporalQueryFilters, type TenantScopeOptions, type Timestamp, VectorProvider };
package/dist/index.mjs CHANGED
@@ -45,6 +45,9 @@ var StoreTarget = {
45
45
  VECTOR: "vector",
46
46
  GRAPH: "graph"
47
47
  };
48
+
49
+ // ../shared/src/types/principal.ts
50
+ var SINGLE_TENANT_ID = "_single";
48
51
  export {
49
52
  DEFAULTS,
50
53
  EmbeddingProviderName,
@@ -52,6 +55,7 @@ export {
52
55
  MemoryServerError,
53
56
  MemoryType,
54
57
  RAGStrategy,
58
+ SINGLE_TENANT_ID,
55
59
  SensitivityLevel,
56
60
  StoreTarget,
57
61
  VectorProvider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyxmate/memory",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "description": "SDK for pyx-memory — Memory as a Service for AI agents",
6
6
  "license": "MIT",
@@ -7,14 +7,17 @@ description: >
7
7
  files or images, or search for what was discussed before. Also use when
8
8
  integrating the pyx-memory SDK into code, working with MemoryClient, or using
9
9
  the HTTP API. Covers multi-tenant isolation (tenantId, TENANT_MODE),
10
+ fine-grained ReBAC (namespaces, principals, authz tuples, AuthzPlan),
10
11
  sensitivity classification, encryption at rest, and confidence/abstention.
11
12
  Triggers on: 'remember', 'recall', 'store memory', 'search memory',
12
13
  'ingest file', 'store this image', 'remember this document',
13
14
  'what did we decide', 'pyx-memory', 'memory', 'MemoryClient',
14
15
  'integrate memory', 'memory consolidation', 'multi-tenant',
15
16
  'tenant isolation', 'sensitivity', 'encryption', 'confidence',
16
- 'abstention', 'access control', 'RBAC', 'read-only', 'permissions',
17
- 'agent isolation', 'per-agent memory', 'role-based access'.
17
+ 'abstention', 'access control', 'RBAC', 'ReBAC', 'Zanzibar',
18
+ 'namespace', 'principal', 'authz tuple', 'permission', 'permissions',
19
+ 'role', 'role-based access', 'read-only', 'agent isolation',
20
+ 'per-agent memory'.
18
21
  allowed-tools: Read, Grep, Glob, Edit, Write, Bash(curl *)
19
22
  argument-hint: "[store|search|ingest] <content or query>"
20
23
  ---
@@ -126,7 +129,7 @@ For integrating pyx-memory into TypeScript/Bun projects, see the reference docs:
126
129
  |---|---|
127
130
  | Wire pyx-memory into a consumer project | [patterns/consumer.md](patterns/consumer.md) |
128
131
  | Set up embedded memory (full features) | [patterns/embedded.md](patterns/embedded.md) |
129
- | Multi-tenant, RBAC, sensitivity, encryption | [patterns/access-control.md](patterns/access-control.md) |
132
+ | Multi-tenant, ReBAC (namespaces + tuples), sensitivity, encryption | [patterns/access-control.md](patterns/access-control.md) |
130
133
  | SDK quick start, package map, DO/DON'T | [reference/sdk-guide.md](reference/sdk-guide.md) |
131
134
  | HTTP API endpoints | [reference/http-api.md](reference/http-api.md) |
132
135
  | Type signatures and interfaces | [reference/types.md](reference/types.md) |
@@ -9,7 +9,7 @@ Production patterns for restricting who can read and write which memories.
9
9
  - [Pattern 12: Sensitivity-Based Read Restriction](#pattern-12-sensitivity-based-read-restriction)
10
10
  - [Pattern 13: Read-Only vs Read-Write Access](#pattern-13-read-only-vs-read-write-access)
11
11
  - [Pattern 14: Full Production Stack](#pattern-14-full-production-stack)
12
- - [Architecture: Access Policy Layer](#architecture-access-policy-layer)
12
+ - [Pattern 15: Built-in ReBAC](#pattern-15-built-in-rebac-namespaces--tuples--authzplan) ← **start here for fine-grained per-user / per-team access**
13
13
 
14
14
  ---
15
15
 
@@ -22,6 +22,9 @@ Need to isolate agents from each other?
22
22
  Need to isolate organizations/customers?
23
23
  → Use TENANT_MODE=multi + X-Tenant-Id (Pattern 11)
24
24
 
25
+ Need fine-grained per-user / per-team / per-folder access INSIDE a tenant?
26
+ → Use built-in ReBAC: namespaces + principals + authz_tuples (Pattern 15)
27
+
25
28
  Need to hide sensitive data from some users?
26
29
  → Use sensitivity classification + X-Caller-Access-Level (Pattern 12)
27
30
 
@@ -29,7 +32,7 @@ Need read-only vs read-write roles?
29
32
  → Use API_KEY + ADMIN_API_KEY + an API gateway (Pattern 13)
30
33
 
31
34
  Need all of the above?
32
- → Pattern 14 (full production stack)
35
+ → Pattern 14 (full production stack) + Pattern 15 (ReBAC)
33
36
  ```
34
37
 
35
38
  ---
@@ -256,7 +259,7 @@ function memoryAccessMiddleware(req: Request, userRole: string): boolean {
256
259
 
257
260
  ## Pattern 14: Full Production Stack
258
261
 
259
- Combines all patterns for a production multi-tenant deployment with sensitivity, encryption, and role-based access.
262
+ Combines all patterns for a production multi-tenant deployment with sensitivity, encryption, and the built-in two-tier API key model (`API_KEY` for read+write, `ADMIN_API_KEY` for destructive ops). For fine-grained per-user/per-namespace access inside a tenant, layer Pattern 15 (ReBAC) on top of this base.
260
263
 
261
264
  ### Server config
262
265
 
@@ -331,51 +334,136 @@ function createDashboardClient(tenantId: string, userId: string) {
331
334
 
332
335
  ---
333
336
 
334
- ## Architecture: Access Policy Layer
337
+ ## Pattern 15: Built-in ReBAC (namespaces + tuples + AuthzPlan)
338
+
339
+ For fine-grained access control inside a tenant — "alice can read project-X but not project-Y", "team-eng has editor on engineering/*", "revoke without entry rewrite" — pyx-memory ships first-class ReBAC primitives. No external policy service required for v1; the model is Zanzibar-aligned so you can later export to OpenFGA / SpiceDB without changing application code.
340
+
341
+ ### The four primitives
335
342
 
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:
343
+ | Resource | Role |
344
+ |----------|------|
345
+ | `namespace` | The unit you grant access to (folders, projects, channels). Hierarchical via `parentId`. Granting on a parent transitively covers descendants. |
346
+ | `principal` | A subject — `user`, `team`, `group`, `agent`, `service`, or per-tenant `everyone`. Identified by `(tenantId, kind, externalId)`. |
347
+ | `principal_member` | Membership edge `member ∈ group`. Group-of-groups supported (cycles rejected, max-depth-8 namespaces). |
348
+ | `authz_tuple` | The grant: `(subject, relation, object)` — e.g. `(user:alice, viewer, namespace:proj-acme)`. Built-in rewrite: `owner ⊇ editor ⊇ viewer`. |
337
349
 
350
+ ### Server config
351
+
352
+ ```yaml
353
+ # docker-compose.yaml
354
+ environment:
355
+ - TENANT_MODE=multi
356
+ - API_KEY=${MEMORY_API_KEY}
357
+ - ADMIN_API_KEY=${MEMORY_ADMIN_KEY} # required to manage namespaces / tuples
338
358
  ```
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
359
+
360
+ ### Manage namespaces, principals, and tuples (admin API)
361
+
362
+ All `/api/admin/*` routes require `ADMIN_API_KEY` and `X-Tenant-Id`.
363
+
364
+ ```bash
365
+ # 1. Create a namespace (the resource you'll grant access to)
366
+ curl -X POST http://memory:7822/api/admin/namespaces \
367
+ -H "Authorization: Bearer $ADMIN_KEY" \
368
+ -H "X-Tenant-Id: tenant-acme" \
369
+ -H "Content-Type: application/json" \
370
+ -d '{"name":"engineering"}'
371
+ # → {"id":"<NS_ID>", ...}
372
+
373
+ # 2. Register a principal (idempotent on tenant + kind + externalId)
374
+ curl -X POST http://memory:7822/api/admin/principals \
375
+ -H "Authorization: Bearer $ADMIN_KEY" \
376
+ -H "X-Tenant-Id: tenant-acme" \
377
+ -H "Content-Type: application/json" \
378
+ -d '{"kind":"user","externalId":"alice","displayName":"Alice"}'
379
+ # → {"id":"<ALICE_ID>", ...}
380
+
381
+ # 3. Grant alice viewer on engineering
382
+ curl -X POST http://memory:7822/api/admin/authz-tuples \
383
+ -H "Authorization: Bearer $ADMIN_KEY" \
384
+ -H "X-Tenant-Id: tenant-acme" \
385
+ -H "Content-Type: application/json" \
386
+ -d '{
387
+ "subjectKind":"user","subjectId":"<ALICE_ID>",
388
+ "relation":"viewer",
389
+ "objectKind":"namespace","objectId":"<NS_ID>"
390
+ }'
391
+
392
+ # 4. Revoke (one row delete — no entry rewrite, takes effect immediately)
393
+ curl -X DELETE http://memory:7822/api/admin/authz-tuples \
394
+ -H "Authorization: Bearer $ADMIN_KEY" \
395
+ -H "X-Tenant-Id: tenant-acme" \
396
+ -H "Content-Type: application/json" \
397
+ -d '{ ...same body as the grant... }'
346
398
  ```
347
399
 
348
- ### Why this is better than building ACL into the memory store
400
+ ### Make something public to the whole tenant
349
401
 
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.
402
+ ```bash
403
+ # Resolve the per-tenant `everyone` principal (auto-created)
404
+ curl -X POST http://memory:7822/api/admin/principals \
405
+ -H "Authorization: Bearer $ADMIN_KEY" \
406
+ -H "X-Tenant-Id: tenant-acme" \
407
+ -H "Content-Type: application/json" \
408
+ -d '{"kind":"everyone","externalId":"_everyone"}'
353
409
 
354
- ### Implementation approach
410
+ # Grant `everyone` viewer on the announcements namespace
411
+ curl -X POST http://memory:7822/api/admin/authz-tuples \
412
+ -H "Authorization: Bearer $ADMIN_KEY" \
413
+ -H "X-Tenant-Id: tenant-acme" \
414
+ -H "Content-Type: application/json" \
415
+ -d '{
416
+ "subjectKind":"everyone","subjectId":"<EVERYONE_ID>",
417
+ "relation":"viewer",
418
+ "objectKind":"namespace","objectId":"<NS_ID>"
419
+ }'
420
+ ```
421
+
422
+ Reversing later is the same — delete the tuple, visibility flips back the next request.
423
+
424
+ ### Search-time enforcement
425
+
426
+ Pass the calling principal on `Memory.search()`. The server computes an `AuthzPlan` (visible namespaces + cached revision) BEFORE any retrieval source fans out. SQLite/FTS, LanceDB vector search, and the graph engine all apply the plan as a native pre-filter — forbidden entries never enter RRF fusion or reranker scoring (which would skew normalization for everyone).
427
+
428
+ ```typescript
429
+ const result = await memory.search({
430
+ query: 'Q4 revenue',
431
+ strategy: 'hybrid',
432
+ principal: {
433
+ tenantId: 'tenant-acme',
434
+ principalId: 'alice',
435
+ kind: 'user',
436
+ },
437
+ });
438
+ // result.entries contains only namespaces alice can `view`,
439
+ // plus legacy NULL-namespace entries (tenant-root bucket).
440
+ ```
441
+
442
+ ### Legacy compatibility
443
+
444
+ Entries with `namespace_id IS NULL` (the default before ReBAC) remain visible to anyone with tenant access. You opt entries into ReBAC by setting `namespaceId` at ingest:
355
445
 
356
446
  ```typescript
357
- // Define scopes as metadata on memory entries
358
447
  await memory.store({
359
448
  content: 'Q4 revenue projections',
360
449
  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
- },
450
+ metadata: {},
451
+ namespaceId: '<NS_ID>',
366
452
  });
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
453
  ```
380
454
 
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.
455
+ No bulk migration is requiredlegacy data keeps working unchanged.
456
+
457
+ ### Why not metadata + post-filter?
458
+
459
+ The pre-PR-D pattern was "tag entries with `metadata.allowedTeams`, filter results in your gateway". That breaks RAG retrieval:
460
+
461
+ 1. **Ranking pollution** — RRF fusion and reranker interpolation normalize against the candidate set. If forbidden entries enter retrieval, they shift the scoring for the entries the caller IS allowed to see.
462
+ 2. **Confidence corruption** — abstention scores depend on score variance over the candidate set. Hidden entries change the variance.
463
+ 3. **Write amplification** — changing a role means rewriting metadata on every affected chunk.
464
+
465
+ ReBAC tuples solve all three: pre-filter at the SQL/vector layer, single-row mutations, no entry rewrites.
466
+
467
+ ### Next-scale: external PDP
468
+
469
+ When you outgrow the in-process tuple store (~10M tuples, or you need cross-tenant sharing, or distributed enforcement), the tuple format is Zanzibar-compatible — export to OpenFGA or SpiceDB and swap the `AuthzPlan` compute path to call their `ListObjects` API. The retrieval-engine code does not change.
@@ -60,6 +60,28 @@ const client = new MemoryClient('http://localhost:7822', process.env.MEMORY_API_
60
60
  | GET | `/api/memory/query-as-of?asOf=...` | Bi-temporal point-in-time query (asOf, type, agentId, source, limit) |
61
61
  | GET | `/api/memory/query-by-event-time?startTime=...&endTime=...` | Bi-temporal event time range query (startTime, endTime, type, agentId, source, limit) |
62
62
 
63
+ ## ReBAC Admin (11 endpoints)
64
+
65
+ Fine-grained access control inside a tenant — namespaces, principals, group membership, and authz tuples. All routes require `ADMIN_API_KEY` and an `X-Tenant-Id` header. See [`patterns/access-control.md`](../patterns/access-control.md) Pattern 15 for the full guide.
66
+
67
+ | Method | Endpoint | Description |
68
+ |--------|----------|-------------|
69
+ | POST | `/api/admin/namespaces` | Create namespace (JSON: `{ name, parentId?, metadata?, createdBy? }`) |
70
+ | GET | `/api/admin/namespaces` | List namespaces in this tenant |
71
+ | DELETE | `/api/admin/namespaces/:id` | Delete namespace + tuples referencing it (RESTRICT on children) |
72
+ | POST | `/api/admin/principals` | Upsert principal (JSON: `{ kind, externalId?, displayName?, metadata? }`) |
73
+ | GET | `/api/admin/principals` | List principals in this tenant |
74
+ | DELETE | `/api/admin/principals/:id` | Delete principal + cascade memberships |
75
+ | POST | `/api/admin/principal-members` | Add member to group (JSON: `{ groupId, memberId }`) — cycle-checked |
76
+ | DELETE | `/api/admin/principal-members/:groupId/:memberId` | Remove membership |
77
+ | POST | `/api/admin/authz-tuples` | Write tuple (JSON: `{ subjectKind, subjectId, subjectRelation?, relation, objectKind, objectId, createdBy? }`) |
78
+ | GET | `/api/admin/authz-tuples?objectId=&subjectId=` | List tuples (filterable) |
79
+ | DELETE | `/api/admin/authz-tuples` | Delete tuple (JSON: same body shape as POST) |
80
+
81
+ `kind`: `'user' | 'team' | 'group' | 'agent' | 'service' | 'everyone'`. `objectKind` is `'namespace'` only in v1. `relation` is freeform (built-in rewrite: `owner ⊇ editor ⊇ viewer`).
82
+
83
+ Cross-tenant safety: every route verifies the referenced resource belongs to the caller's tenant; mismatches return `404` (not `403`) to avoid existence disclosure.
84
+
63
85
  ## File Ingestion (Images + Documents)
64
86
 
65
87
  `POST /api/memory/ingest/file` accepts multipart/form-data with:
@@ -185,9 +185,10 @@ interface MemoryEntry {
185
185
  source?: string; // filename, URL, session ID
186
186
  eventTime?: string; // when event happened (bi-temporal)
187
187
  ingestTime?: string; // when stored (bi-temporal)
188
- tenantId?: string; // multi-tenant isolation
189
- userId?: string; // user within tenant
190
- teamId?: string; // team/group within tenant
188
+ tenantId?: string; // multi-tenant isolation (hard boundary)
189
+ userId?: string; // user within tenant (legacy soft-scope)
190
+ teamId?: string; // team/group within tenant (legacy soft-scope)
191
+ namespaceId?: string; // ReBAC resource coordinate; NULL = legacy tenant-root bucket
191
192
  sensitivity?: SensitivityLevel; // auto-classified: 'public' | 'internal' | 'secret'
192
193
  encrypted?: boolean; // true when content is encrypted at rest
193
194
  pinned?: boolean; // exempt from consolidation (decay-archive + dedup-merge); use for stable anchors referenced by external systems
@@ -209,6 +210,11 @@ interface MemorySearchParams {
209
210
  tenantId?: string; // multi-tenant scoping
210
211
  userId?: string; // user-level scoping
211
212
  teamId?: string; // team-level scoping
213
+ /** Calling principal — drives ReBAC AuthzPlan. Search applies it as a
214
+ * native pre-filter on SQLite/FTS, vector, and graph layers; forbidden
215
+ * entries never enter RRF / reranker / abstention. Optional. */
216
+ principal?: PrincipalContext;
217
+ namespaceIds?: string[]; // populated internally from AuthzPlan — do not set directly
212
218
  maxSensitivity?: SensitivityLevel; // filter by max sensitivity level
213
219
  abstentionThreshold?: number; // 0-1, below this confidence → shouldAbstain=true (default: 0.3)
214
220
  }
@@ -223,6 +229,66 @@ interface SearchFilters {
223
229
  }
224
230
  ```
225
231
 
232
+ ## PrincipalContext + ReBAC Types (v0.16.0+)
233
+
234
+ The calling identity used to compute search-time AuthzPlan. Comes from
235
+ authenticated request context (`X-Tenant-Id` + `X-User-Id` / `X-Agent-Id`),
236
+ NEVER from request bodies.
237
+
238
+ ```typescript
239
+ interface PrincipalContext {
240
+ tenantId: string; // hard boundary; SINGLE_TENANT_ID sentinel in single mode
241
+ principalId: string; // userId | agentId | service identifier
242
+ kind: 'user' | 'agent' | 'service';
243
+ }
244
+
245
+ interface Namespace {
246
+ id: string;
247
+ tenantId: string;
248
+ name: string;
249
+ parentId?: string; // adjacency tree, max-depth 8
250
+ createdAt: string;
251
+ createdBy?: string;
252
+ metadata: Record<string, unknown>;
253
+ }
254
+
255
+ interface Principal {
256
+ id: string;
257
+ tenantId: string;
258
+ kind: 'user' | 'team' | 'group' | 'agent' | 'service' | 'everyone';
259
+ externalId?: string; // maps to userId / agentId / teamId on entries
260
+ displayName?: string;
261
+ createdAt: string;
262
+ metadata: Record<string, unknown>;
263
+ }
264
+
265
+ interface AuthzTuple {
266
+ tenantId: string;
267
+ subjectKind: PrincipalKind;
268
+ subjectId: string;
269
+ subjectRelation?: string; // for usersets like "members of team-eng"
270
+ relation: 'viewer' | 'editor' | 'owner' | string;
271
+ objectKind: 'namespace'; // v1 only supports namespace-level grants
272
+ objectId: string;
273
+ createdAt: string;
274
+ createdBy?: string;
275
+ }
276
+
277
+ interface AuthzPlan {
278
+ tenantId: string;
279
+ visibleNamespaceIds: string[]; // resolved + transitively expanded
280
+ visibleEntryOverrides: string[]; // reserved for v1.1
281
+ revision: string; // bumps on every tuple/principal/membership mutation
282
+ }
283
+ ```
284
+
285
+ Built-in relation rewrite: `owner ⊇ editor ⊇ viewer`. Adding new relations
286
+ is a code change, not a schema migration.
287
+
288
+ In-process API: `Memory.authz` exposes `AuthzStore` with `createNamespace`,
289
+ `upsertPrincipal`, `addMember`, `writeTuple`, `deleteTuple`, `buildAuthzPlan`
290
+ etc. Sidecar / HTTP API: see [`http-api.md`](./http-api.md) §ReBAC Admin.
291
+
226
292
  ## MemoryOptions Reference
227
293
 
228
294
  > **Embedding is internal** — pyx-memory uses `LocalEmbeddingProvider` with BGE-M3 (1024d) automatically. You do NOT pass an `embedder` option.