Skip to main content

Bi-temporal PermissionedDataPoint

Every node and every edge in the KB carries the same envelope:

class TemporalRange(BaseModel):
valid_from: datetime # event time start
valid_to: Optional[datetime] # event time end (None = still true)
created_at: datetime # system time start
expired_at: Optional[datetime] # system time end (None = active)

class PermissionedDataPoint(BaseModel):
id: UUID
type: str = "PermissionedDataPoint"
temporal: TemporalRange
acl: ACL # owner + role-based + ABAC + ReBAC anchors
provenance: Provenance # dataset_id + data_id + extractor chain
layer: LayerMembership # PRIVATE / HIERARCHICAL / MARKETPLACE / GLOBAL
index_fields: list[str] # which fields feed the vector embedding
properties: dict[str, Any]

Two timelines

Following the Graphiti / Zep four-timestamp model:

PairTracksCloses when
valid_from / valid_toReal-world event timeFact stops being true
created_at / expired_atSystem ingest timeFact is logically invalidated

A contradicted edge closes valid_to (and optionally expired_at + invalidated_by_edge_id) — it is never deleted. This preserves the timeline for as_of=<past_ts> queries.

Provenance chain

Provenance carries dataset_id + data_id + the extractor chain (["spacy", "gliner", "llm"]) + the pipeline run id. When a tenant requests targeted forgetting (GDPR / CCPA), KBRuntime.forget locates rows by dataset/data id and closes their validity window.

ACL envelope

The ACL block carries:

  • owner_principal_id + owner_tenant_id (RBAC anchor).
  • roles_read / roles_write / roles_delete (RBAC).
  • abac_tags (ABAC — region, classification, time-of-day, ...).
  • rebac_anchor_ids (OpenFGA tuple keys like document:abc#viewer).
  • deny_principal_ids (explicit denial list).

DefaultPermissionResolver (in kb-permissions.md) fuses all four into a single per-request AccessBitmap.

Bi-temporal merge in the composer

KBLayerComposer.compose_recall collects hits across layers (private > hierarchical > marketplace > global), then applies the precedence-aware bi-temporal merger:

  1. Group hits by entity id.
  2. The first occurrence (highest precedence) wins.
  3. Lower-precedence hits get appended to metadata.dissenting_layers so the UI can surface them transparently.
  4. valid_from/valid_to are preserved on every hit so a downstream as_of reconstruction is lossless.