Repository navigation
Lorebase 0.1.0
First release. Lorebase keeps a typed ontology in Postgres and lets teams change it on branches, review the change in a Proposal and merge schema and data together, with Conflicts resolved per Attribute. Agents read through a REST API that never calls a model, or through the MCP server, and write only through Operations; a write that needs review becomes a task branch and a Proposal. Documents are extracted into Proposals whose items reviewers accept or reject. Pre-alpha and pre-1.0: /api/v1 is not frozen and interfaces may change in a minor version. The database schema is at migration 0012. The lines of this first section link the documentation of each feature; from the next release they link the pull request or issue.
Added
Schema and versioning (design, schema as code)
- A Domain is a YAML schema document (
format: lorebase.schema/v1) with Kinds, Attributes, Relations, Traits, Operations, Derivations and Sources, checked statically and applied as one schema change set:POST /api/v1/domains/{domain}/branches/{branch}/schema/changes,lorebase schema validate|diff|apply. Attribute ids are stable, so a rename keeps its history. - Content-addressed Revisions, branches, tags, diff and time-travel reads (
?at=<revision>,lorebase diff,lorebase record get --at). - Three-way merge of schema and data with typed Conflicts (attribute value, delete or modify, add or add, missing value, type coercion, cardinality, dangling Link and more), resolved with
ours,theirs,baseor a custom value, one by one or from a file (lorebase proposal conflicts|resolve). Merge policiesprefer, cascade and defaults. The merged data is validated against the merged schema before the commit. - Proposals with reviews, required approvals (
keeper.proposals), Conflict resolution and merge over REST and CLI (lorebase proposal create|review|merge|close). The default branch of a Domain is protected (keeper.domains.protect_default_branch): changes reach it through a Proposal. - Expressions in CEL for Operations (preconditions, steps, review policies) and for
expressionandaggregateDerivations, type checked when a schema is applied (schema.expressions_enabled) and recomputed in the materialized head on every commit.pathDerivations are not computed.
Reads for people and agents (agents, performance)
- Records, Links, the JSON query DSL, traversal, neighborhoods and lexical search over REST (
lorebase record,query,search), with ETags and the serving Revision in every response. Full-text search reads GIN indexes on(domain_id, branch, search)and(domain_id, branch, title gin_trgm_ops)(migration0011,btree_gin), so a search reads only its own branch's matches, however many branches share the partition. Semantic search takes vectors supplied by the caller; nothing computes embeddings. - Context packs within a token budget (
POST .../context,lorebase context), Record explanations with the origin of each value (GET .../records/{key}/explain,lorebase explain) and data health reports (GET .../health,lorebase health), all computed without a model. - A change feed as Server-Sent Events and as polling (
GET .../branches/{branch}/events,.../changes) and signed webhooks (POST .../webhooks,Lorebase-SignatureHMAC). - Overlay branches (store schema notes, migration
0010): a branch created withmode: overlay(POST .../branches, defaultstore.materialize.branch_mode, always for names underagent/,extract/andsource/, and for agent task branches perkeeper.agent_writes.branch_mode) stores only the rows it changes and reads its base as of the fork point. Creating one does not depend on the size of the Domain; it is promoted to a full copy abovestore.materialize.overlay_max_rowsrows oroverlay_max_base_lag_revisionsgenerations of base lag, before a schema change and before its base is deleted.lorebase branch create --mode copy|overlaychooses the mode from the command line. Overlay reads go through_overlayvariants of the security definer functions with the same argument and scope checks. An overlay gets the per-Attribute indexes of its schema on its own rows, built asstore.indexjobs when it is created, and its neighborhood looks Links and Records up with one index scan over both branches (migration0012); on the 100k-Record baseline search, attribute queries, Links of a Record and the neighborhood stay within 30% of a copy at p50 (performance). - Per-Attribute indexes and security definer read functions that keep Domain isolation under row-level security. At 99,900 Records and 302,940 Links on one laptop, the measured p95 of every agent read stays within its budget (performance); the 1M-Record baseline is not measured.
Writes and ingestion (quickstart, scribe, inference)
- Built-in Operations
import_records,import_linksandimport_batchwith dry run and idempotency keys, and custom Operations declared in the schema (POST .../operations/{operation}/execute). lorebase import records|links|batchload CSV, TSV, JSON Lines or JSON (Parquet with theparquetextra): types are converted from the schema, a Record travels with its Links, files are cut into batches of one Revision each.- Schema inference from CSV, TSV, JSON, JSON Lines (
.jsonl,.ndjson) and Parquet uploads, chosen by suffix (keeper.schema_inference.file_formats), and from Postgres tables (POST .../schema:infer): Kinds, typed Attributes, keys, Relations with cardinality, a confidence and the evidence for every decision. It proposes and writes nothing. Parquet uploads need thelorebase-keeper[parquet]extra; without it they are refused with a 422 naming it. A record profiler (lorebase_store.profiling.profile_records,RecordProfiler) andprofile_sourcein Scribe profile any Source through its connector; limits are instore.profiling.records. - CSV and Postgres Sources with snapshot and incremental syncs run by Scribe, the worker and scheduler on the Postgres job queue. A sync commits on a Source branch, opens a Proposal and applies the Source's merge policy (
proposal,auto_when_clean,auto_with_authority); every changed value keeps its row as Lineage (lorebase sources sync|runs). - JSON, JSON Lines, Parquet and HTTP JSON Sources (scribe): the
jsonandjsonltypes and therecords_path,flatten_separatorandhttpoptions (headers as secret references, cursor, page, offset or Link header pagination, a watermark parameter and a page cap). Files are opened under the allowed roots without following links, URLs are checked against private addresses, JSON Lines and Parquet are streamed, and an HTTP JSON run that passesmax_pagesfails instead of committing part of a snapshot. Parquet needs thelorebase-scribe[parquet]extra. - Document intake and tiered extraction into Proposals (extraction):
POST .../documentstakes Markdown, plain text, PDF and DOCX (PDF and DOCX with the scribedocumentsextra); Scribe parses and chunks them and proposes Records, Links and values with verified evidence, from rules (tier 0, no model), local GLiNER and GLiREL (tier 1) or any OpenAI-compatible model with JSON schema output (tier 2, cached, token budget per job). Reviewers accept, edit or reject items one by one or by filter (GET .../proposals/{p}/items,POST .../items:decide,lorebase documents upload,lorebase proposal items|decide); nothing reaches the default branch without a merge. Derived and sensitive Attributes are never extracted.GET .../documents/{id}/text(lorebase documents text) serves the parsed text the evidence offsets index. The web app has a Documents screen (intake, library, a viewer that highlights the evidence in the parsed text) and an Items tab on extraction Proposals to accept, edit or reject items one by one or by filter. - Inference providers behind one OpenAI-compatible interface for chat and embeddings, plus local fastembed, GLiNER and GLiREL (optional extras), and compose profiles for Ollama and a LiteLLM proxy. Extraction uses them; nothing computes embeddings yet.
Agents and access (MCP, operations)
- Herald, the MCP server, over stdio and Streamable HTTP with 11 tools (
lorebase_context,lorebase_query,lorebase_executewith dry run and more), two resources and one prompt. It is a thin layer over Keeper and holds no database credentials.lorebase_executepassesintent,auto_proposeandauto_mergeto Keeper also when the call names notask, and returns Keeper's problem text unchanged;auto_propose: truewithout ataskis refused asvalidation-failed. - Agent writes that need review (agents): an Operation refused with
409 review-required(protected branch or review policy) runs, when the call names atask, on a task branchagent/{principal}/{task}and opens or updates one Proposal, merged at once only whenkeeper.agent_writes.allowed_roles_for_auto_mergeallows the caller's role, nothing conflicts and the required approvals are in. Without ataskthe409carriesnexthints. Settingskeeper.agent_writes, overridable per Domain. - API keys (hashed with a server-side pepper), OIDC bearer tokens, role grants per Domain, Kind, Operation and branch, and a hash-chained audit log (
lorebase apikey,GET .../audit).python -m lorebase_keeper bootstrapcreates the first installation administrator key from the database settings and prints it once; it refuses when an administrator already holds a usable key, unless--force. - Rate limits per principal and request class (
RateLimit-*headers,429withRetry-After) and daily quotas per API key, shown bylorebase whoami(keeper.limits.rate,keeper.limits.quota). Failed authentications are counted per client address and per key prefix;keeper.limits.rate.auth_failures.per_key_prefix_enabled: falsecounts them per client address only, so that someone who knows a key prefix cannot keep that key throttled. - Domain backup and restore bundles (
python -m lorebase_store.bundle export|import) and garbage collection of unreachable Revisions with a dry run (python -m lorebase_store.gc).
Clients and web app (web app, tour)
- The
lorebasecommand line and a Python client, sync and async, checked against the committed OpenAPI document (docs/openapi.json). Exit codes are documented: 0 success, 1 usage or not found, 2 validation or conflict, 3 authentication or permission, 4 server error. - A web app with Explorer, Canvas, Modeler (including a Start from data wizard over schema inference that accepts CSV, TSV, JSON, JSON Lines and Parquet files), Review with a Conflict resolver and an Items tab for extraction Proposals, Documents, Sources and Admin screens. It runs on the mock API or on Keeper, with end-to-end tests against a real Keeper for browsing, Branches, schema changes, Review and Settings. It reads
/config.js(window.__LOREBASE_CONFIG__:apiMode,keeperBaseUrl, an optionalapiTokenor anyVITE_*name) over its build values, so thelorebase-webimage switches to live mode at run time, fromVITE_*variables of the container or the chart'sweb.config. Screens load on demand: the main chunk is 193 kB, vendor libraries sit in their own chunks, andpnpm buildfails when the main chunk or the first load exceedsapps/web/bundle-budget.json. The Modeler and the Sources editor keep the Source options they do not show when they save a schema. - The five-day demo:
make demobrings up a dedicated compose stack and runs the proof scenario of design 3.2 with the CLI, in under a minute once the image is built (docs/demo.md).
Deployment (deployment, kubernetes)
- Docker compose for Postgres 16 with pgvector, migrations, Keeper and Scribe, with profiles
local-models,litellm,observabilityandscale-out(a second Keeper, a Postgres streaming replica and nginx; read replica routing with a freshness guard). - A Helm chart (
deploy/helm/lorebase) for Keeper, a migration hook, Scribe, Herald, the web app, Ollama, Ingress, autoscaling and read replica DSNs. The Postgres parameters of the CloudNativePG cluster are explicit (postgresql.cluster.postgresql.parameters,max_connections: "100"by default); size it above Keepers x (pool_size+max_overflow) plus Scribe, migrations and administration. It passeshelm lint --strict,helm templateand kubeconform; it has not run on a live cluster. - An observability bundle in
deploy/observability: Grafana dashboards for Keeper, Store and Scribe, Prometheus alert rules with a runbook per alert and the OpenTelemetry Collector configuration (operations). The composeobservabilityprofile loads them; the Helm chart renders them as ConfigMaps withobservability.enabled=true. - A metrics catalogue (docs/metrics.md) with the Prometheus series, labels and source of every metric, among them the response cache, authentication failures, commit duration, merge attempts, security definer read calls, jobs claimed and queue depth, webhook deliveries, garbage collection runs and bytes freed, and extraction items. A test fails when a dashboard or alert rule names a series that is not emitted, or when an instrument is missing from the catalogue. Latency histograms declare explicit bucket boundaries from 5 ms to 10 s.
- Release artifacts: images
ghcr.io/aantenore/lorebase(Keeper, Scribe and Herald share it; the command picks the service) andghcr.io/aantenore/lorebase-web, tagged0.1.0,0.1andlatest, signed with cosign keyless and carrying a CycloneDX SBOM attestation; CycloneDX SBOMs and the Helm chart package on the GitHub release. The nine Python workspace packages go to PyPI only when the maintainers configured publishing.
Documentation
- A documentation site at aantenore.github.io/lorebase, built from
docs/with Material for MkDocs (make docs,make docs-serve) and published on every push tomain. The command line and configuration reference pages are generated from the code (make docs-generate;scripts/docs/generate.py --checkfails on a stale page) and the REST API page renders the committeddocs/openapi.json.
Security
- A security review of the trust boundaries before this release (docs/security-review.md) fixed all 23 findings, among them: a Source could resolve any secret reference, including the database DSN (critical), Source errors echoed resolved secrets,
keeper.auth.mode: nonecould be served on a non-loopback address (now refused unlesskeeper.auth.allow_insecure), webhook targets in non-global address ranges were accepted, and a slow webhook target could stall delivery for every Domain. - Compose: Keeper and Scribe connect as an application role without superuser or
BYPASSRLS(LOREBASE_APP_USER,LOREBASE_APP_PASSWORD, created by the one-shotdb-roles), so row-level security applies to them; the owner role runs the migrations and index maintenance. Thelitellmandobservabilityprofiles refuse to start whileLITELLM_MASTER_KEYorGRAFANA_ADMIN_PASSWORDis empty,change-meor shorter than 16 characters. - Migration
0008adds argument, scope andsearch_pathchecks to the security definer read functions and refuses to run while the application role may create objects in their schemas; migration0009adds client network allowlists to API keys (allowed_networks,lorebase apikey create --allow-network, behindkeeper.trusted_proxies). - Bundle import is bounded and refuses links, schema inference sources are configured per Domain, the OIDC issuer and signing keys need
https, CSV Sources check the resolved address and open files without following links, failed authentications are rate limited per client address and key prefix, the Helm NetworkPolicy is on by default with narrow egress, provider error text is scrubbed, and open event streams are authorized again at an interval.
Not done yet
- Lineage and history endpoints beyond the lineage summary on Record reads.
- The 1M-Record performance baseline and the 10M-Record targets of the design.
- Merge policies
authorityandnewest_wins. - Schema suggestions from extraction; embedding jobs and hybrid search (semantic search ranks stored vectors against a query vector the caller supplies);
pathDerivations. - Events declared by Operations (the outbox and signed webhooks carry Revision events only), sensitivity masking, the PII vault, and Traits in time-travel reads.
- Web app: OIDC login with BFF sessions, the Operation, Derivation and Source editors of the Modeler, and a Canvas that renders 5k nodes and 10k edges smoothly.
- A command or endpoint that verifies the audit chain, LinkML import and export, the threat model, the proof scenario as an automated test in CI, and the stability promise for
/api/v1that comes with 1.0.
Upgrade notes
There is no earlier release. For a new installation, run the migrations (alembic upgrade head, migrations 0001 to 0012) before Keeper starts: the compose migrate service and the Helm hook do it. Migration 0011 creates the btree_gin extension when it is missing (it ships with Postgres contrib); on a database that already holds data it builds the search indexes concurrently per partition, without blocking writes. Keeper's shipped configuration accepts every request as an administrator (keeper.auth.mode: none); use api_key or oidc for anything shared, give api_key modes a pepper of at least 32 characters, list the administrator in keeper.auth.installation_admins and create the first key with python -m lorebase_keeper bootstrap.