Generated banner for Engrava
Generated logo for Engrava

Engrava

Memory frameworkOfficial Sovantica project

Created by Sovantica

Published in sovantica/engrava. Publisher page

12 GitHub stars

What it is

The memory database for AI agents - graph memory, hybrid search, tamper-evident thought/edge journal.
— the description by Sovantica

AutoGineer overview, from the brain’s own files

Engrava is an in-process memory database designed for AI agents, built on SQLite. It provides a structured way to store and retrieve agent thoughts using a graph-based model, where thoughts are linked by typed edges. The system supports hybrid search, combining vector similarity, full-text search (FTS5), recency, and priority. It includes built-in mechanisms for memory consolidation (dreaming), memory hygiene (archiving and garbage collection), and a tamper-evident journal for tracking mutations. Engrava operates as a library with no external service dependencies in its core path, allowing agents to maintain persistent, queryable state locally.

How it works

Memory model: Hybrid

AutoGineer overview, from the brain’s own files

Engrava uses a SQLite database to store thoughts and their relationships. It organizes data into thoughts and typed edges, which can be queried using the MindQL language. The system supports hybrid search by combining vector embeddings, keyword search, and metadata signals. It manages memory through a lifecycle that includes archiving and garbage collection, and it can consolidate memories into reflections. The system is extensible via hooks and provides an MCP server for integration with various AI agent frameworks.

Folders
FolderRoleFiles
.githubagent-config16
docsdocs37
examplesexamples13
scriptsscripts24
srcsource-code89
teststests201
.githooksfolder1

The agent reads

Requirements

Runtimes: Python >=3.11

Databases: SQLite

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)

Set it up

  1. This brain runs code automatically: 2 scripts. Review .githooks/commit-msg, Makefile before opening the folder in Claude Code. No step below turns a hook on for you.

    • .githooks/commit-msg: runs on git commit-msg
    • Makefile: echo "$(BLUE)>> Installing engrava + dev dependencies$(RESET)"; $(PIP) install --upgrade pip setuptools wheel; $(PIP) install -e ".[dev]"; bash scripts/install_hooks.sh; echo "$(GREEN)>> Done. Run 'make check' to verify quality gate.$(RESET)"
  2. Not scanned by AutoGineerIts dependencies are installed by pip or uv when you run it and were not scanned by AutoGineer (5 declared). https://pypi.org/project/engrava/
  3. Created by Sovantica. Published in sovantica/engrava. Licence: MIT License.

Usage

AutoGineer overview, from the brain’s own files

  • Persistent memory for autonomous AI agents
  • Personal knowledge base management
  • Conversation storage with semantic retrieval
  • Research notes with associative linking
  • Memory-backed agent development
  • Audit-trailed thought and edge tracking
“Store a new observation about a project requirement.”
The agent uses the remember() method to persist the thought, which is then indexed for future hybrid search and graph traversal.
“Retrieve relevant context for a coding task.”
The agent uses the recall() method, which performs a hybrid search across FTS5, vector, and graph signals to return ranked, contextually relevant thoughts.
“Query the memory graph for all observations with high priority.”
The agent executes a MindQL query: FIND thoughts WHERE thought_type = 'OBSERVATION' AND priority = 'P1'.
“Consolidate old memories to free up space.”
The agent triggers the DreamingExtension to cluster related thoughts and compute reflections, or uses the memory hygiene loop to archive low-signal thoughts.

Documentation by Sovantica

Engrava

The memory database for AI agents.

A queryable memory graph with bi-temporal valid-time predicates — in an in-process Python library over one SQLite file, with no generative-model call in the core write path.

Image: CI Image: PyPI Image: Python Image: License: MIT

Engrava is a standalone, in-process database for AI agent memory. Built on SQLite, it provides thought CRUD, edge-based knowledge graphs, embedding-based similarity search, full-text search (FTS5/BM25), and a declarative extension system — all in a single package with zero external service dependencies.

Benchmark results are published, and the runs are reproducible from a separate repository. Every published run is Group A — memory_pipeline_llms: [], no language model anywhere in the memory layer's built-in path. That is a property of the architecture's default signals and hooks rather than a measurement, unless a custom Dreaming signal or Memory Hygiene hook is configured to call one.

  • Benchmark results — the live table: every published row with the comparability segment it belongs to.

  • engrava-benchmark — the runner. Clone it and reproduce a result against the package from PyPI. MIT.

  • Discussions — questions; reproduction questions belong in the Q&A category.

Use Cases

  • AI agent persistent memory

  • Personal knowledge base

  • Conversation storage with semantic search

  • Research notes with associative linking

  • Any application that needs a thought-graph with embeddings

Quick Start

Installation
pip install engrava

Optional extras:

pip install 'engrava[vec]'                # sqlite-vec vector search backend
pip install 'engrava[embeddings-local]'   # sentence-transformers embeddings (local model)
pip install 'engrava[embeddings-openai]'  # OpenAI-compatible embeddings API
pip install 'engrava[embeddings-ollama]'  # Ollama local embeddings server
pip install 'engrava[embeddings-hf]'      # HuggingFace Inference API embeddings

Dreaming/consolidation and the knowledge graph need no extra — they are part of the base install.

embeddings-local carries a large first-run download. It pulls sentence-transformers and torch, plus a further, separate model download (~88 MB for all-MiniLM-L6-v2, cached under ~/.cache/huggingface/hub) on first use. On Linux, the PyPI build of torch also pulls the NVIDIA CUDA libraries as dependencies: several gigabytes, most of it usable only with an NVIDIA GPU. Without one, install the CPU-only build first (see Configuration: No NVIDIA GPU for the exact command and numbers). engrava-mcp[local] is the exact same download, reached through the server package instead of this one. If you do not need semantic search in-process, pip install engrava alone (no extra, no download, no model) already gives you keyword search, the graph, and MindQL — see Configuration → Quick-start profiles for that and the Ollama-backed alternative that keeps the model out of this process entirely.

Basic Usage

Store a memory and search for it in two calls — no IDs to generate, no record to assemble:

import asyncio

import aiosqlite

from engrava import SqliteEngravaCore


async def main() -> None:
    # SqliteEngravaCore wraps an open aiosqlite connection.
    async with aiosqlite.connect(":memory:") as conn:
        conn.row_factory = aiosqlite.Row
        store = SqliteEngravaCore(conn)
        await store.ensure_schema()

        await store.remember("Python is great for AI agents")
        await store.remember("SQLite needs no server")

        result = await store.recall("what language is good for agents?")
        for thought_id, score in result.results:
            thought = await store.get_thought(thought_id)
            if thought is not None:
                print(f"{thought.essence}  (score: {score:.3f})")


asyncio.run(main())

remember() stores the text as a thought (generating its ID for you) and returns the stored ThoughtRecord; recall() runs the same hybrid search as search_hybrid() and returns the ranked results. This store has no SearchConfig of its own, and it still ranks with the same default weights — FTS 0.30, vector 0.55, recency 0.10, priority 0.05, graph 0.00 (opt-in) — as a store built with one; see Hybrid Search. For full control — setting priority, thought type, metadata, or the cognitive cycle on a write — build a ThoughtRecord yourself and call create_thought().

From here, link thoughts with typed edges, query them with MindQL, or run the full ingest → dream → search tour in the Quick Start guide.

Configuration-Driven Setup
from engrava import SqliteEngravaCore

# from_config opens and OWNS the connection — use it as an async context manager.
async with await SqliteEngravaCore.from_config("engrava.yaml") as store:
    # The schema is already applied by from_config.
    thought = await store.get_thought("some-id")

See docs/configuration.md for the full YAML schema.

Upgrading

Automatic schema migration runs on first connection. See the upgrade guide for compatibility notes, backup guidance, and troubleshooting steps.

Features

Thought CRUD

Create, read, update, and archive thoughts with full lifecycle management. All models are frozen Pydantic objects — mutations happen via evolve().

Edge-Based Knowledge Graph

Link thoughts with typed, weighted edges. Edge types include ASSOCIATED, DEPENDS_ON, DERIVED_FROM, CONSOLIDATED_FROM (created by dreaming), and CONTESTED_BY.

Store embeddings alongside thoughts and search them with the built-in NumPy cosine backend or the optional sqlite-vec backend (pip install 'engrava[vec]'). Pluggable embedding providers:

ProviderExtraBackend
SentenceTransformerProviderembeddings-localLocal model via sentence-transformers
OpenAICompatibleProviderembeddings-openaiAny OpenAI-compatible API
OllamaProviderembeddings-ollamaLocal Ollama server
HuggingFaceProviderembeddings-hfHuggingFace Inference API
CallbackProvider(built-in)Custom callable
Full-Text Search (FTS5)

SQLite FTS5 virtual table with BM25 ranking. Hybrid search combines vector similarity, text relevance, recency, priority, and graph connectivity. Signals that cannot run for a query are skipped and the remaining weights are redistributed.

MindQL Query Language

Declarative query language for the thought-graph:

FIND thoughts WHERE thought_type = 'OBSERVATION' AND priority = 'P1' LIMIT 10
COUNT thoughts WHERE lifecycle_status = 'ACTIVE'
SELECT thought_id, essence FROM thought WHERE thought_type = 'BELIEF'

Extensible with custom commands through MindQLExecutor or an ExtensionManifest; the lifecycle hook registry is reserved and is not consulted by the core executor.

Extension System

Plug into the thought lifecycle via EngravaHooksProtocol. Subclass DefaultEngravaHooks when you only need selected active methods:

from engrava import DefaultEngravaHooks, ThoughtRecord

class MyHooks(DefaultEngravaHooks):
    async def on_store(self, thought: ThoughtRecord) -> ThoughtRecord:
        # Observe or enrich the object returned after persistence.
        return thought

    async def decay_function(
        self, thought: ThoughtRecord, elapsed_cycles: int
    ) -> float:
        # Supply a decay multiplier to an enabled Memory Hygiene pass.
        return 1.0

Core currently invokes on_store, on_retrieve, and decay_function. on_store runs after the source thought's row is inserted; changing its return value does not rewrite the persisted row. score_function and mindql_extension_registry() remain reserved protocol methods and are not called by core.

Dreaming / Memory Consolidation

Built-in DreamingExtension for periodic memory consolidation — scores thoughts via its default signals (no LLM calls), promotes high-value entries, and creates REFLECTION thoughts by clustering semantically related thoughts and computing centroid embeddings through a deterministic structural function, not an LLM. A custom signal you register with DreamingExtension runs whatever code it contains. Available since 0.3.0.

→ See docs/benchmarks.md for reproducible evidence (synthetic benchmark suite runnable in ~5 minutes).

Forgetting / Memory Hygiene

The subtractive half of memory maintenance, paired with Dreaming: an opt-in loop whose built-in scoring makes no LLM calls, that archives cold, low-signal thoughts — the default action, reversible via restore_thought — and, as a separately opted-in step, garbage-collects them (not reversible) once both restore windows have elapsed under their non-zero defaults — a cycle count and a wall-clock duration, either of which can be configured to 0 to disable that window. OFF by default; once enabled, archived thoughts drop out of default retrieval and can be restored (restore_thought / include_archived).

→ See docs/memory-hygiene.md for the loop, protection, restore windows, and the honest deletion posture.

Tamper-Evident Thought/Edge Journal

Opt-in hash-chain journal that records thought and edge mutations (plus action status/verification_status transitions) as SHA-256-linked, before/after entries — a tamper-evident thought/edge journal, not a whole-database audit (embeddings and action creation are not covered). Off by default, one config flag to enable. Query history with store.journal.get_entries(...) and validate the chain with store.verify_journal(), which audits whatever chain is on disk independent of the current journal.enabled state — the store.journal writer handle does not exist while journaling is off, but the chain earlier sessions wrote is still in journal_entry and still needs verifying.

→ See docs/audit-trail.md for enabling, querying, verification, and the security model (what "tamper-evident" does and does not guarantee).

Multi-Service Isolation

Run multiple independent databases under one EngravaManager:

from pathlib import Path

from engrava import EngravaManager

async with EngravaManager(data_dir=Path("./data")) as mgr:
    agent_a = await mgr.get_store("agent-a")
    agent_b = await mgr.get_store("agent-b")
    # Completely isolated databases
MCP Server

Want Engrava as a memory server for your agent? The MCP server ships as its own package, engrava-mcp — a native stdio server (no HTTP shim) with read tools, optional write tools, attachable engrava:// resources, and guided prompts, for any MCP client (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code):

uvx engrava-mcp        # or: pip install engrava-mcp

engrava-mcp pulls engrava in transitively, so installing it also gives you the import engrava library. See the engrava-mcp package for install, client configuration, the full tool/resource/prompt reference, and read-only mode.

uvx vs. a persistent install. uvx engrava-mcp (equivalently uv tool run engrava-mcp) installs into "an ephemeral virtual environment in the uv cache directory" per uv's own --help text — fine for the lexical/network profiles above, which add nothing heavier than httpx. Once you are on the local profile (engrava-mcp[local], the same sentence-transformers + torch download as engrava[embeddings-local]), uv tool install 'engrava-mcp[local]' is the better fit: it installs once into a persistent environment — the same uvx engrava-mcp invocation then reuses that installed environment instead of resolving a fresh ephemeral one — and a later uv tool upgrade engrava-mcp re-pays only the changed packages, not the whole dependency tree.

CLI

engrava --db mydata.db info          # Database stats
engrava --db mydata.db query "FIND thoughts WHERE thought_type = 'OBSERVATION' LIMIT 5"
engrava --db mydata.db snapshot -o backup.jsonl
engrava --db mydata.db restore -i backup.jsonl
engrava --db mydata.db gc            # Collect ARCHIVED thoughts + their edges/embeddings/actions
engrava --db mydata.db migrate       # Ensure schema is up-to-date
engrava --db mydata.db export -o portable.json

gc physically deletes ARCHIVED thoughts together with every edge touching one on either end — including edges whose other end is still live — their embeddings and the actions sourced from them, then reconciles the vector index by removing every vec0 row no embedding row owns; on a vec0-indexed store where sqlite-vec cannot be loaded — most commonly because engrava[vec] is not installed — a pass that is about to delete stops before deleting anything and exits 1 rather than stranding those vectors in an index nothing can then reach them through.

engrava info's --format json output carries every field of the metrics snapshot exposed by await store.metrics(), with the snapshot's own schema version renamed metrics_schema_version, plus two fields the snapshot itself does not carry, db_path and database_schema_version — see Upgrade Guide. The default text output is a shorter summary of that same data, not the full snapshot.

See the CLI reference for every command and option.

Architecture

  • SQLite with WAL mode for concurrent reads

  • Frozen Pydantic models — immutable domain objects

  • Async-first — all I/O via aiosqlite

  • Hook-based extension — zero monkey-patching

  • Template method pattern — subclass SqliteEngravaCore for extended schemas

  • Zero external services — everything runs locally in-process

Documentation

  • Documentation Index — choose a path by task: learn, build, operate, extend, or migrate

  • Core Concepts — the mental model (thought, edge, reflection, cycle, …) — start here

  • The Bi-temporal Model — the optional valid-time axis: query a fact as of any instant, invalidate without deleting

  • Positioning — when Engrava is (and isn't) the right tool, and how it compares

  • Quick Start — 5-minute setup guide

  • Tutorial — build a small notes memory end to end

  • Recipes — copy-paste snippets for common tasks (store a turn, retrieve context, TTL, dedup, …)

  • Building a memory-backed agent — the end-to-end agent turn loop (ingest → retrieve → generate → consolidate)

  • Migrating from another memory system — concept mapping, porting calls, bulk import, and scoping/multi-tenancy

  • Embeddings — wiring a real embedding provider (local / OpenAI / Ollama / HuggingFace / custom)

  • MCP server (engrava-mcp) — expose a store to MCP clients (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code) via the standalone server package: install, run, client config, tools/resources/prompts

  • Configuration — YAML config format and options

  • Upgrade Guide — compatibility matrix, backups, and troubleshooting

  • Extensions — Writing custom extensions and hooks

  • Observability — Metrics snapshot API

  • Audit Trail — Tamper-evident hash-chain journal (enabling, querying, verifying, security model)

  • Evidence and Conflicts — model claims, provenance, contested facts, and caller-owned clarification

  • Security — trust boundaries, data egress, tenant isolation, encryption posture, and journal limits

  • Error Handling and Recovery — decide when to retry, repair input, or replace a store

  • Benchmarks — release gates, versioned observed values, and LongMemEval

  • API Reference — Full protocol and class reference

  • CLI Reference — every engrava command and option

  • Glossary — quick definitions of every Engrava term

  • MindQL — Query language syntax and examples

  • Troubleshooting — symptom → cause → fix for common errors

  • FAQ — quick answers (LLM/keys, embeddings-optional, scale, concurrency, backups, …)

  • Performance & Scaling — the vector-backend switch, bulk-ingest, and dreaming cost at scale

  • Data Lifecycle & Retention — lifecycle states, TTL, archive-vs-delete, GDPR erasure, disk reclamation

  • Deployment — process model, database files on disk, containers, graceful shutdown

  • Concurrency — the WAL single-writer model, busy timeout, and per-service isolation

  • Backup & Recovery — WAL-safe backups, snapshot vs file copy, restore verification

  • Known Limitations — Platform notes and constraints

Development

make install   # deps + dev extras + local git hooks -- from the primary checkout
make gate      # fast: hooks-active + lint + format check + type check + goldens drift, ~70s
make check     # lint + format check + type check + the full test suite with coverage (unchanged; no hooks-active, no goldens drift)

make install also wires a commit-msg hook that lints the message of the commit a squash merge creates — including on a local branch that never opens a pull request, where nothing else does. It refuses to install from a linked worktree (core.hooksPath is shared across all of them). See CONTRIBUTING.md for what it covers and what it does not.

License

MIT — see LICENSE for details.

docs/security.md

Security and Trust Boundaries

Engrava is an in-process memory database, not an authentication gateway. It runs inside the caller's Python process and stores data in SQLite. This page defines Engrava's security boundary and the controls an application or operator must provide around it.

For vulnerability reporting and disclosure timelines, see the repository Security Policy. This guide covers deployment and data trust; it does not replace that reporting policy.

Default posture: local and in-process

The core library opens a local SQLite database and exposes an in-process Python API. It does not start a network listener or require an external service. With no embedding provider or custom extension configured, normal store operations require no network access.

Network access can still enter the deployment through components selected by the caller:

  • remote or remotely addressed embedding providers;

  • custom hooks, manifest factories, and MindQL handlers;

  • application, service, or MCP wrappers around the library;

  • dependency or model acquisition performed by an optional provider.

Treat those components as separate trust decisions. The fact that Engrava core runs in-process does not make arbitrary extensions or provider endpoints local.

Data at rest

Engrava uses ordinary SQLite through aiosqlite. It does not provide built-in database encryption, encryption keys, or field-level encryption. The database can contain thought text, metadata, provenance, actions, embeddings, and audit-journal deltas. In WAL mode, recent data may also be present in the adjacent -wal file; the -shm file belongs to the same operational file set. Logical snapshots are JSONL data and are not encrypted by Engrava either.

Protect the entire database directory, not only the main .db file:

  • run Engrava under a dedicated service identity and restrict directory access to that identity;

  • set an appropriately restrictive process umask before the database or service directory is created;

  • use an OS-, filesystem-, or volume-level encryption facility when stored memory requires encryption at rest;

  • protect snapshot files, physical backups, temporary copies, and exported logs under the same policy as the live database;

  • avoid network filesystems for a live WAL database; use a suitable local or properly configured persistent volume.

Engrava needs read/write access to the directory so SQLite can create and manage the -wal and -shm files. Engrava does not configure encrypted volumes or manage their keys. See Deployment for file-layout and permission details.

Embedding providers and data egress

Embedding generation sends the input text to the configured provider. That can include newly stored thought content, re-embedded content, and search queries. Vectors generated for stored or re-embedded content are persisted locally and are sensitive derived data; they should receive the same storage protection as the source text. A vector generated for a search query is used transiently for that query and is not inserted into the embedding table.

Provider boundaries are:

ProviderData path
sentence-transformerEmbedding inference is local after the model is available. Model-loading dependencies may acquire model files according to their own cache and network configuration.
openai-compatibleSends text to the configured HTTP API; the default endpoint is OpenAI.
huggingfaceSends text to the Hugging Face Inference API.
ollamaSends text to the configured Ollama endpoint; the default is loopback, but base_url can point elsewhere.
callbackWraps a caller-supplied synchronous function, run on a worker thread. The data path is whatever that function does — local computation, a remote call, or anything else the callback contains — and is not something engrava constrains or can describe generically.

Before enabling a remotely addressed provider:

  • confirm the endpoint's retention, training, residency, and incident-response terms for the data you send;

  • use TLS for non-loopback endpoints and do not assume a custom base_url is secure merely because the provider name is trusted;

  • apply outbound network policy so the process can reach only approved hosts;

  • avoid sending secrets or regulated content unless the endpoint is approved for it.

For supported remote providers, YAML accepts a whole-value environment reference such as api_key: "${EMBEDDING_API_KEY}". Prefer runtime secret injection over a literal credential in a tracked configuration file. Engrava resolves the value into process memory; it is not a secret manager and does not rotate, scope, or revoke credentials. The built-in OpenAI-compatible provider does not itself interpolate the request's own Authorization header into its error text, but a non-success HTTP response's error includes the raw response body verbatim; a server that echoes a header or token back in that body can still put it in the raised message. Application logging and third-party clients remain part of the surrounding threat model.

Tenant isolation and authorization

filters=, visibility=, thought visibility, metadata ownership fields, and ProvenanceContext.actor_id are not authorization controls. They are caller-supplied data or query refinements. A caller can omit a filter, forge metadata, use another API, or access SQLite directly if it already has the connection or file.

Engrava core performs no authentication, principal validation, row-level authorization, or write-policy enforcement. For strong cross-tenant isolation, use a separate database file per tenant or security domain, for example through EngravaManager, and enforce the tenant-to-store mapping in trusted application code before returning a store handle. Also isolate filesystem credentials and backups where the threat model requires it. A database-per-tenant layout limits accidental cross-querying; it does not protect against a process identity that can read every tenant file.

If a shared corpus is required, authorization belongs in a trusted service layer that mediates every read and write. Do not rely on a convention that each caller will always pass the correct visibility filter.

Extensions, hooks, and migrations

Extensions are trusted code. A configured hook class is dynamically imported and instantiated in the Engrava process. Manifest factories, MindQL handlers, CLI commands, and discovered entry points also execute Python from installed packages. Hooks can inspect records passed to them, modify returned records, perform I/O, and contact the network.

An extension manifest may declare SQL migrations. Engrava validates migration shape, rejects transaction-control statements, applies each migration under a savepoint, and records filenames and checksums to detect later historical drift. A savepoint makes each migration file and its bookkeeping atomic; a later-file failure does not roll back earlier files that were already committed. Those checks provide consistency, but they do not establish publisher identity or make the SQL safe to accept from an untrusted package. A migration executes against the store connection and can change stored schema or data.

Use the extension boundary deliberately:

  • install extensions only from a reviewed and trusted source;

  • pin and verify package versions through the deployment's supply-chain controls;

  • prefer explicit manifest paths for library-created stores;

  • review new hook behavior and every pending migration before deployment;

  • back up the database before applying core or extension migrations.

Discovery boundaries differ between the library and CLI:

  • SqliteEngravaCore does not scan entry points by itself. Library discovery happens only when the caller invokes discover_manifests() or configuration sets manifests.discover: true.

  • root engrava --help and resolution of an otherwise unknown CLI command scan engrava.cli; built-in commands resolve without that scan;

  • engrava query additionally scans engrava.extensions to register MindQL commands from discovered manifests.

The CLI scans load Python entry-point objects even though they do not apply the manifests' schema migrations. Use global --no-extensions, or set ENGRAVA_DISABLE_EXTENSIONS=1, to prevent loading from both engrava.cli and engrava.extensions, including for help, built-ins, and query. This control does not override explicit manifest configuration used by library code. Installing an extension package into an environment where discovery is enabled grants that package code execution in the CLI process. Use a dedicated virtual environment containing only reviewed packages, or use the Python API with an explicit manifest allow-list when this trust is unacceptable.

See Extension Hooks, Extensions, and the Upgrade Guide for the execution contracts.

Snapshot and restore trust

engrava restore validates known record types, column sets, required values, and value types, and applies the restore transactionally. This prevents a malformed record from leaving a partial restore and keeps snapshot-controlled column names out of generated SQL. It does not attest the origin or truth of the content.

Use logical restore for snapshots whose origin and integrity you trust. Text and metadata from an untrusted snapshot become untrusted stored content and may later be returned to an agent, passed to an LLM, or sent to an embedding provider. Review external snapshots as imports, including for instruction-like or otherwise adversarial content. Restore only from a completed file that is no longer being modified.

A logical snapshot excludes the audit journal. A fresh restore target therefore starts with an empty journal, and restore --clear empties an existing target's journal along with the data it wipes. A restore without --clear merges into the target — and if that target's journal is non-empty, it refuses any record that collides with an existing row (primary key or UNIQUE constraint) and rolls the whole restore back, rather than silently replacing it. This gate never applies to an empty journal, the ordinary case, since journaling is opt-in. --orphan-journal-entries allows the merge anyway and restores the prior behaviour, where it can orphan journal entries even when no incoming ID collides with what the journal already describes: a duplicate (from_thought_id, to_thought_id, edge_type) triple replaces an existing edge, and replacing a thought cascades to that thought's own edges, embeddings, and actions — neither needs its own ID to collide. The journal entries describing the earlier row stay behind; verify still reports the chain as valid even though it no longer matches what is stored. A physical backup preserves the journal but is tied to the database file and must be captured with a WAL-safe method. See Backup & Recovery for the supported boundary and procedures.

Audit journal threat model

The optional journal is a keyless SHA-256 hash chain stored in the same SQLite file as the records it describes. It can detect accidental corruption and an edit to a retained entry, or to the linkage between retained entries, that does not recompute the chain from that point forward. It cannot detect the deletion of a self-consistent suffix — including every entry — since nothing in the store proves how long the chain should be; that requires an externally captured high-water mark (see Audit Trail → verifying integrity). It is not a signature, an HMAC, or cryptographic non-repudiation.

An actor who can rewrite the database can alter journal entries and recompute all subsequent hashes. The journal also covers a defined mutation subset, not the whole database: thought and edge mutations plus action status transitions; embeddings, action creation, and read-derived access telemetry are outside the chain.

Within the entries it does cover, the chain binds ordering and content (sequence_number, parent_hash, mutation_type, target_id, delta) but not timestamps: neither created_at nor entry_id is in the hash preimage. Rewriting every created_at produces a fully backdated trail that still verifies. Moving a timestamp across a get_entries(since=...) lower bound also changes that window in either direction — downward removes an entry from an audit window, upward inserts one into it — so a time-bounded audit query cannot compensate: it reads the same unprotected column. Treat journal timestamps as informative and anchor any time-bounded claim on a (sequence_number, entry_hash) pair captured externally.

The preimage joins those five fields with | and no escaping, writing an absent field as the empty string. Their separation therefore rests on what the fields themselves can hold — a decimal sequence_number, one of seven fixed mutation_type literals, a SHA-256 hex parent_hash, and a delta that is always a JSON object dump whose quoting a caller-chosen target_id cannot imitate — rather than on the encoding. Those grammars hold for the entries the store emits; they are not enforced by JournalWriter.append(), which is exported, so a caller writing entries through it directly can produce two distinct entries with one preimage and one hash. That takes in-process code execution against your own store, which this threat model already treats as past the boundary — but read the binding as a property of the store's field grammars, not as one the format guarantees. Verification also re-serialises the stored delta before hashing it, so the chain binds that delta's decoded value and not its stored bytes: a rewrite of the blob's key order, whitespace, or escaping that preserves the value verifies clean. See Audit Trail → security model.

For a stronger control:

  • enable scheduled verification or journal.verify_on_open where its linear startup cost is acceptable;

  • restrict database write access to the intended writer;

  • periodically export the latest journal entry_hash to an append-only, WORM, signed, or independently controlled system;

  • compare the verified local chain tail with that external anchor during audits and incident response.

Engrava does not publish or manage an off-box anchor for you. See Audit Trail for exact coverage and verification APIs.

Backup, retention, and erasure

Security controls must follow all copies of the data. Use a WAL-safe physical backup or the documented logical snapshot flow, encrypt backup storage where required, restrict restore privileges, and test recovery using non-production destinations.

Archiving is reversible retention, not erasure. A hard delete removes the live row but audited content can remain in journal deltas, and old copies can remain in backups. Under SQLite's default auto_vacuum=NONE, which engrava does not override, freed pages do not shrink the file until a rebuild such as VACUUM; a database externally configured with auto_vacuum=FULL can truncate freed tail pages automatically at commit. An erasure process must account for the live database, the journal, snapshots and physical backups, replicas or exports, and the retention policy of any remote provider that received the text. Deleting a non-tail journal entry breaks the retained chain's linkage and requires an explicit re-baseline if journal verification is retained; deleting a self-consistent suffix, including every entry, does not make local verification fail on its own — see Audit journal threat model above.

delete_thought issues its own deletes for the thought's edge, embedding, and action rows, in the same savepoint as the parent delete, rather than depending on the ON DELETE CASCADE that only exists from core schema 12 onward. A dangling embedding row left by a delete made under an older engrava build still carries its vector in the database file; engrava migrate purges these rows. See Deletion on a database that has not been migrated.

See Data Lifecycle for the complete retention and erasure procedure.

Service and MCP boundaries

Engrava core does not expose a network service and therefore does not implement service authentication, transport encryption, request authorization, rate limits, or internet-facing input policy. The MCP server is a separate package, and any HTTP, RPC, worker, or agent-tool wrapper is outside the core trust boundary.

The wrapper or deployment must authenticate callers, authorize every operation, map principals to the correct store, protect transport, constrain request size and concurrency, handle quotas and abuse, and avoid exposing raw database paths or unrestricted store handles. Network exposure should be private by default and enabled only with controls appropriate to the environment.

Deployment checklist

  • [ ] Use a dedicated runtime identity and restrict the full database directory.

  • [ ] Place the database, WAL files, snapshots, and backups on approved encrypted storage when encryption at rest is required.

  • [ ] Select local or approved remote embedding providers and restrict outbound destinations.

  • [ ] Inject provider credentials at runtime; do not commit literal keys.

  • [ ] Enforce authentication and authorization outside Engrava core.

  • [ ] Use a separate database per tenant or security domain where isolation is required.

  • [ ] Install only reviewed hooks and extensions; disable CLI entry-point loading with --no-extensions or ENGRAVA_DISABLE_EXTENSIONS=1 when it is not needed.

  • [ ] Back up before migrations and use a WAL-safe backup method.

  • [ ] Treat restored content as trusted only when its source is trusted.

  • [ ] Enable and verify the journal only with its keyless, partial-coverage threat model understood; anchor the chain externally when required.

  • [ ] Include journal residue, backups, exports, and provider retention in erasure procedures.

  • [ ] Apply transport, authorization, rate-limit, and abuse controls in every service or MCP layer that exposes Engrava.

402 files · 357 not served here

Advantages

Limitations

Security

Risk level: Low risk

This brain runs

Git hooks (1)

  • .githooks/commit-msg (commit-msg): runs on git commit-msg

Agent instructions that run or fetch something (4)

  • AGENTS.md: Run `git commit` and it fires on the patterns it checks for.
  • CLAUDE.md: pip install -e ".[dev]"
  • CLAUDE.md: Run a clean test suite locally before pushing;
  • CLAUDE.md: Branch from `dev`, follow Conventional Commits, run the local quality gate before pushing.

Install scripts (1)

  • Makefile (make install): echo "$(BLUE)>> Installing engrava + dev dependencies$(RESET)"; $(PIP) install --upgrade pip setuptools wheel; $(PIP) install -e ".[dev]"; bash scripts/install_hooks.sh; echo "$(GREEN)>> Done. Run 'make check' to verify quality gate.$(RESET)"
  • Low risk: scripts-present (.githooks/commit-msg)
  • Low risk: network-instructions (CLAUDE.md:40)
  • Low risk: network-instructions (pyproject.toml:98)
  • Low risk: scripts-present (scripts/install_hooks.sh)

Attribution and licence

Created by
Sovantica
Declared authors
  • Sovantica (declared in pyproject.toml)
Publisher
Sovantica (Official Sovantica project)
Source
sovantica/engrava · commit f6a8b73
Licence
MIT License (files served here)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Sovantica.

Install from source

357 of its files are not served here, so there is no ZIP. sovantica/engrava at f6a8b73