Akita — memory your AI can use, that you can't read
Akita gives AI apps per-user memory and search the operator provably cannot read: text sealed with AES-GCM, embeddings encrypted with CKKS, keys that never leave the user's side — and search fast enough to ship (~95 ms warm end-to-end, measured, results identical to plaintext search).
(Live version: .venv/bin/python demos/demo_page/app.py — real cryptography, no mocks.)
Status: v0.2 prototype. Real cryptography, honest measurements, not audited — don't protect production data with it yet (SECURITY.md). v0.2 hardened the trust path: tamper-evident storage, client-verified audit, stronger key protection (CHANGELOG.md).
Install
pip install akita-fhe sentence-transformers
from akita import Memory, MemoryServer, MiniLMEmbedder
server = MemoryServer() # the blind side
mem = Memory.open("user1", "a-passphrase-only-the-user-knows", server, MiniLMEmbedder())
mem.add(["dr chen increased the propranolol dose to 80mg"])
mem.search("what medication changes has my doctor made?", k=3)
# -> [{'id': 0, 'score': 0.54, 'text': 'dr chen increased the propranolol dose to 80mg', ...}]
The trust boundary: text is embedded on the key holder's side, sealed
with AES-GCM (each blob bound to its id, the index to a rollback counter,
so substituted or rolled-back server data fails closed), and its
embeddings are CKKS-encrypted before anything leaves. The server stores
blobs it cannot open and answers searches with one ciphertext×ciphertext
multiply per packed segment — it never sees the query, the scores, or the
ranking, and it refuses enrollment of any context containing a secret
key. mem.audit() fetches what the server holds and verifies client-side
that it cannot decrypt; mem.wipe() is crypto-shredding.
See both sides live: .venv/bin/python demos/demo_page/app.py →
http://127.0.0.1:8642 — your device on the left (ask anything), the
operator's actual view on the right: hex blobs, ciphertext segments,
secret key: NO, and which fetches it observed (winners + decoys,
indistinguishable). Real cryptography, no mocks.
Measured (400 sensitive notes, real MiniLM embeddings, M1 Pro)
| Metric | Value |
|---|---|
| Store 400 notes (embed + encrypt + upload) | 0.8–2.4 s (~2–6 ms/note) |
| Semantic search, end to end | 0.24 s median (~95 ms warm) |
| Server compute per search (encrypted corpus) | 145 ms — planner predicted 145 ms |
| Storage at rest | 45.5 KB/memory |
| One-time enrollment (public keys) | 1.9 MB |
| Delete | tombstone immediate; compact() re-packs physically |
| Operator's view | no secret key (client-verified by audit()), hex-opaque blobs and segments |
Honest v0 costs, stated plainly: the search download scales with corpus
size (~230 KB per 8-memory segment — server-side aggregation is the
roadmap fix); fetch patterns are padded with decoy reads pending a PIR
stage; and key loss means memory loss — that's what "the operator cannot
read it" costs. Full run: demos/demo6_private_memory.py, numbers in
results/.
The planner: know the cost before you encrypt
The other half of the library. FHE tooling fails silently and costs unpredictably; the planner makes layout decisions explicit, validated, and costed before anything encrypts:
from akita import PipelineSpec, plan
print(plan(PipelineSpec("matvec_ranking", dim=384, n_items=1_000_000)).explain())
Akita plan — matvec_ranking
security CKKS, N=8192, modulus 200 bits (<= 218 for 128-bit), depth 1
layout dim 384 -> padded 512 (validated: divides 4096 slots), 245 chunk(s) x 4096 items
est compute 639.74 core-s (127.95 s wall on 10 cores)
est traffic 326 KB up / 59,976 KB down (+ 33.8 MB one-time public context)
est cost $0.008885 per query at full utilization ($0.017771 on a dedicated 10-core box) @ $0.05/core-hr
warning CKKS results must be revealed only to the secret-key holder. ...
Two facts from building this repo explain why it exists:
- Silent corruption is one layout mistake away. A 384-dim encrypted
matmul in TenSEAL returns wrong scores with no error because 384
doesn't divide the slot count
(
tests/test_planner.py::test_the_trap_is_realreproduces it). The planner makes that layout unrepresentable. - The cost model holds. The planner predicted 13.1 s for a 100k-doc encrypted search this laptop measured at 14.0 s — within 7%.
v0 plans three workload shapes (matvec_ranking, column_scoring,
memory_search), refuses what shouldn't run under CKKS (comparisons →
routed to TFHE or the client), and executes plans on TenSEAL. Cost
constants are calibrated on this machine; expect ±30% elsewhere.
Why any of this is fast enough: overhead is a workload-shape choice
The "100,000× overhead" of homomorphic encryption prices one encrypted op against one plaintext op. Amortized over packed SIMD slots and priced per answer, the picture changes — and it changes by orders of magnitude depending on workload shape. Measured on one laptop core, same 128-bit security:
| Workload | Server compute | Overhead vs plaintext | Cost |
|---|---|---|---|
| Encrypted search: query vs 16,384 docs (dim 128) | 2.6 s (0.54 s on 10 cores) | ~6,600× | $0.0022 / 1M docs |
| Encrypted scoring: logistic model, 4,096 records | 9.4 ms | ~330× | $0.00003 / 1M records |
| Exact rule (TFHE): 2 comparisons + AND/OR | 11.3 s / decision | ~10⁸× | route around it |
Correctness: search top-10 identical to plaintext (max error 5×10⁻⁸);
scoring error 4×10⁻⁸; TFHE exact by construction. In every demo the
server holds a public context only — the scripts assert it cannot
decrypt. An end-to-end private search engine over HTTP (12,288 real docs,
80/80 top-10 exact, 2.26 s median) lives in demos/private_rag/, with a
102,400-doc scale test (~$0.002/query).
Caveats that keep these numbers honest: baselines are best-of-10
single-thread numpy (FAISS-class baselines would raise the multipliers
~2–5×; the per-answer economics don't move); scale-test and prototype
figures price a dedicated machine for the query's wall time, while the
planner also reports the full-utilization basis — the two bracket real
deployments. The full reasoning with sources: docs/why.md.
Security notes & limitations — read before relying on this
- The privacy boundary ends where the prompt begins. Akita protects memory storage and search: the operator cannot read stored memories, queries, scores, or rankings. But once your app decrypts retrieved memories and inserts them into an LLM prompt, their privacy depends on where inference runs — a local model, your own VPC, or a third-party API. Akita cannot protect what your app sends elsewhere; design your inference path accordingly and say so in your own privacy policy.
- Parameters: all CKKS contexts use N=8192 with ≤200-bit total coefficient modulus — within the 218-bit cap for 128-bit security in the homomorphicencryption.org standard. TFHE (Concrete) runs on its default parameter selection; we don't override its failure-probability target, so treat it as Concrete's default rather than a bound we claim.
- IND-CPA-D / result disclosure: CKKS is approximate. Decrypted results are safe to use by the key holder; revealing them to other parties (including the server) enables key-recovery-style attacks unless noise flooding is applied (Li–Micciancio, and the 2024 CCS line of work). Every plan the planner emits carries this warning. The demos here never disclose results beyond the key holder.
- Access patterns: fetching top-k results by ID reveals which entries matched (never the query or the scores). Decoy fetches pad this; closing it fully requires a PIR stage — production-proven elsewhere (Apple), not yet implemented here.
- Model extraction (scoring workloads): decrypted scores leak model information over many queries; rate-limit as you would any ML API.
- Integrity: FHE guarantees privacy, not that the server ran the right function. Verifiable FHE is still research.
- This is a prototype: TenSEAL/SEAL and Concrete are real libraries, but this repo's glue code is unaudited. Do not deploy as-is.
Layout
akita/— the SDK:memory.py(encrypted per-user memory),planner.py,runtime.py,embedder.pytests/— 23 tests incl. exactness-vs-plaintext, server blindness, tamper/rollback detection, the silent-corruption reproduction, and cost-model validationdemos/demo_page/— the live split-screen demodemos/demo6_private_memory.py— Private Memory measured end to enddemos/private_rag/— private search engine over HTTP + scale testdemos/demo1..3_*.py— the micro-benchmarks (CKKS search, CKKS scoring, TFHE rules)results/— every number in this README, as JSONdocs/why.md— the technical reasoning, with sources
Developing from source
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e ".[demos,dev]"
.venv/bin/python -m pytest tests/ -q
or plain pip: python3.12 -m venv .venv && .venv/bin/pip install -e ".[demos,dev]".
Exact tested versions: requirements.lock.txt (Python 3.12.13, macOS
14.6, M1 Pro). Requires Python 3.10–3.12 (TenSEAL has no 3.13+ wheels).
Contributing, security, roadmap
Contributions that don't require cryptography expertise are the most
valuable right now — adapters, embedders, clients, and benchmarks on your
hardware: see CONTRIBUTING.md. Vulnerabilities:
privately, per SECURITY.md. Where this is going (and
what it deliberately isn't): ROADMAP.md. License:
Apache-2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file akita_fhe-0.2.0.tar.gz.
File metadata
- Download URL: akita_fhe-0.2.0.tar.gz
- Upload date:
- Size: 30.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7108daedf78e7bc5afc00b2292726742507d41c9c57efa5fb0f9f06803eae3be
|
|
| MD5 |
21820e958abd37f04bfc2ed900e035c6
|
|
| BLAKE2b-256 |
063519042bea4370ebcfcbb184b8c7fdd2dcd0e5184c2d94f3873f51876eb5d1
|
Provenance
The following attestation bundles were made for akita_fhe-0.2.0.tar.gz:
Publisher:
publish.yml on gauthierpiarrette/akita
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
akita_fhe-0.2.0.tar.gz -
Subject digest:
7108daedf78e7bc5afc00b2292726742507d41c9c57efa5fb0f9f06803eae3be - Sigstore transparency entry: 2301566767
- Sigstore integration time:
-
Permalink:
gauthierpiarrette/akita@2986e24d3d8366dd89ffc4f0351ba8826a5961b2 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/gauthierpiarrette
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2986e24d3d8366dd89ffc4f0351ba8826a5961b2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file akita_fhe-0.2.0-py3-none-any.whl.
File metadata
- Download URL: akita_fhe-0.2.0-py3-none-any.whl
- Upload date:
- Size: 23.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1ff9cd603546fe14cc49a1e6eccec588670c963a148443ab687cceae1d48ef7
|
|
| MD5 |
647242dcd0d6027760e0c7cfcd818860
|
|
| BLAKE2b-256 |
1675416ae1f5654c842f921eec1693ea1ea19f02bead4098d36536b3abb7796e
|
Provenance
The following attestation bundles were made for akita_fhe-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on gauthierpiarrette/akita
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
akita_fhe-0.2.0-py3-none-any.whl -
Subject digest:
c1ff9cd603546fe14cc49a1e6eccec588670c963a148443ab687cceae1d48ef7 - Sigstore transparency entry: 2301567065
- Sigstore integration time:
-
Permalink:
gauthierpiarrette/akita@2986e24d3d8366dd89ffc4f0351ba8826a5961b2 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/gauthierpiarrette
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2986e24d3d8366dd89ffc4f0351ba8826a5961b2 -
Trigger Event:
push
-
Statement type: