Engram Alpha is a local-first, single-user tool: one daemon per machine, bound to loopback, holding graph files that live inside your repos. There are no accounts, no cloud, and no telemetry — the threat model is your own machine, your browser, and the AI assistants you connect. The graph itself is sensitive by nature (it is a durable record of your project's reasoning), so it is treated as data worth protecting, not as a cache.
Network surface. The daemon binds 127.0.0.1 only — nothing listens on
external interfaces. The MCP endpoints (/mcp, /projects/{id}/mcp)
validate the Host header against loopback (rmcp's default), which blocks
DNS-rebinding attacks against them. The engram-alpha mcp bridge and the
brief hook talk only to a core they have discovered through engram's own
daemon files and verified over /health — never to an arbitrary port. The
REST API's CORS layer is an origin allowlist — loopback origins and the IDE
webview pseudo-origins the pane embeds under; any other page in your browser
is refused.
Two layers, two protections. Engram stores your knowledge in two places, and they are protected differently on purpose:
curated graph (graph.tepin) |
session history (history.tepin) |
|
|---|---|---|
| Redacted on write | yes | yes, before sealing |
| Encrypted at rest | optional, off by default (0.9.0) | yes, on by default (XChaCha20-Poly1305) |
| You can read it | yes — that is the point | only through the pane/tools |
Since 0.9.0 both layers run the SAME sealing machinery, behind two switches
in the pane's System panel (~/.engram/settings.json). Each store records
its actual state in its own meta, so the daemon can never disagree with
the file on disk; flipping a switch migrates the whole store with a progress
loader, and a migration killed halfway resumes at the next open.
The curated graph defaults to redacted but not encrypted, because it
exists to be inspected: you read it in the pane, you edit it, you delete
from it, and npx tepindb can open it. Sealing it is your call — it buys
at-rest protection at the cost of that raw-file inspectability (the pane
still reads everything: the daemon holds the key).
The history layer defaults to redacted and encrypted, because nobody curates a transcript. It records raw conversation you never reviewed, in bulk, so it cannot rely on your judgment the way the graph does and gets a cipher instead (details below). Recording is opt-in for exactly this reason.
Secrets in memory. Every write — titles, bodies, imports, and history
plaintext before it is sealed — runs a server-side redaction pass
(engram-core/src/redact.rs). It has two layers:
- Named patterns, which are what actually catch secrets: PEM private-key
blocks, AWS access key ids, JWTs, GitHub / Slack / OpenAI-style tokens,
credentials embedded in URLs, and
key = valueassignments for password/token/secret-shaped keys (the value is masked, the key kept so the note still reads). - A high-entropy backstop for opaque tokens with no recognisable shape.
It covers note titles, bodies and string custom-field values. Tags,
code_refs and edge notes are not scrubbed (see Known gaps).
The backstop deliberately does not try to be clever. Since 0.8.7 it judges
entropy per separator-delimited segment rather than over a whole token,
because the previous whole-token rule masked compound technical identifiers —
model slugs like cross-encoder/nli-deberta-v3-small, target triples like
x86_64-unknown-linux-gnu, long URLs — and those losses were permanent and
silent: the original is never stored anywhere, so a memory system quietly
lost the name of its own model. Real credential material has no
dictionary-shaped parts, so segment-level entropy separates the two cleanly. A
token that carries no separators is judged exactly as before.
That relaxation is affordable precisely because of the split above: the curated graph is user-visible and user-curated by design, so an over-aggressive backstop costs real knowledge while buying little, and history — where content arrives unreviewed and in volume — has encryption underneath it rather than resting on redaction alone.
Redaction is defense in depth, not a guarantee. The capture skill instructs assistants never to store secrets in the first place; review the pane, and treat the graph file like you treat your shell history.
Data at rest. Graph stores (.engram/graph.db / graph.tepin) stay
inside the repo, are git-ignored by setup (and checked by doctor), and
never leave the machine unless you explicitly export. The curated graph is
protected by OS file permissions (plus full-disk encryption if you run it —
recommended).
Session history at rest (0.8.4). The history layer
(.engram/history.tepin) records raw assistant conversations, so it gets
more: message and session text is sealed — zstd-compressed, then
encrypted with XChaCha20-Poly1305 under a per-machine 256-bit key minted on
first need and stored in the OS keystore (macOS Keychain / Windows credential
store / Linux secret-service), with a ~/.engram/history.key (0600) fallback (the pre-0.9.0 name, kept — one machine key seals both layers)
for headless machines (ENGRAM_KEYRING=off forces the file). Honest scope:
- Protects: copied
.tepinfiles, backups, stolen disks without FDE, other OS users. Losing the key makes history unreadable; the curated graph is unaffected. - Does NOT protect against malware running as your user — it can read the keystore exactly like the daemon does. That boundary belongs to the OS.
- Stays plaintext, by design: node/edge structure, types, timestamps, session ids — and embedding vectors. Vectors admit inversion attacks that recover the gist of a message, not its text; sealing them would break vector-first retrieval. Stated here so nobody mistakes the layer for more than it is.
- The keyword index is blind (0.9.0): BM25 terms on a sealed store are keyed HMAC-SHA256 digests, never words — term statistics survive (so ranking is bit-identical to plaintext, and history finally has a keyword channel at all), while the index holds no vocabulary. Candidate text is still decrypted in memory at query time only.
- What seals (both layers): titles, bodies, tags, code refs, custom field values, edge notes, and the audit journal's before/after images. Sealed exports still come out as plaintext — an export is a user gesture.
- Redaction runs on the plaintext before sealing — secrets never reach the store, encrypted or not.
Memory poisoning. An assistant can be prompt-injected by hostile content into writing false "knowledge". The trust model limits the blast radius: assistant writes start provisional and decay unless deliberately confirmed; look-alike conflicts are queued for human judgment; every mutation lands in an append-only audit journal with per-session attribution; supersession archives rather than deletes; hard delete is a user-only gesture. The pane is the review surface — writes are silent, but never invisible.
Supply chain. Dependencies are locked (Cargo.lock committed; the
TepinDB driver is pinned by git revision). Binary self-update downloads only
from this repository's GitHub Releases and verifies the artifact against the
release's published SHA-256 before swapping itself. Cortex models download
over HTTPS from their recorded Hugging Face URLs into ~/.cache/engram/.
- The curated graph is plaintext unless you flip the switch. 0.9.0 made graph sealing available (System panel), but the default stays off for inspectability — a store that was never sealed still relies on OS permissions and disk encryption (FileVault/LUKS/BitLocker).
- Deleted history can linger in freed pages. Hard-deleting a session (or wiping the layer) removes the rows, but the storage engine's freed pages aren't scrubbed — the same caveat already documented for curated hard deletes. Sealed rows reduce this to ciphertext residue.
- Tags, code refs and edge notes are not redacted. The scrub covers titles, bodies and custom-field strings; a secret typed into a tag, a code ref or an edge note is stored as written.
- No local authentication. Any process on your machine can use the API — consistent with the single-user local trust model, but stated plainly.
- Model files are not checksum-pinned. Unlike the binary self-update, cortex model downloads verify HTTPS transport but not a pinned digest; adding per-file SHA-256 to the model specs is planned.
Please use GitHub's private vulnerability reporting on this repository. We'll acknowledge within a few days.