EncryptedStore

class EncryptedStore(delegate: CacheStore, cipher: EntryCipher) : CacheStore

A CacheStore that persists no readable prompt or response, wrapped around one that would.

val cache = semanticCache(embedder) {
store = EncryptedStore(PostgresStore(dataSource), myCipher)
}

A decorator rather than a flag on each store, and rather than a step inside dev.kmemo.SemanticCache. Encryption is about what is persisted, and persistence is what a store owns: written here it covers Postgres, Redis, HNSW, a file-backed store and anything a third party writes, in one place, held to the same CacheStoreContract as everything else. Written inside the cache it would have to be repeated for every path that touches an entry and would still not cover a store somebody else wrote.

What the read path costs

One decryption per candidate the delegate returns, not one per lookup. The guards read every candidate's prompt as text, so every candidate has to be readable before the chain can refuse it: a lookup with the default five candidates does five prompt decryptions and one response decryption where an unencrypted store does none. That is the price and it is real. It buys the property that nothing readable is written down, and there is no arrangement that avoids it, because the alternative is a cipher that lets the guards work without decrypting, which means deterministic encryption, which leaks equality across prompts.

The number is a count rather than a duration on purpose: the duration is a property of the cipher you supply, and multiplying it by the candidate count is arithmetic this library cannot do for you.

What still works, and what changes

The guard chain produces exactly the verdicts it produced before, because the cache still sees plaintext: it is the store that holds ciphertext, and everything above it is unchanged. CacheLookup.Hit still reports the matched prompt. Scopes, tags, TTL, eviction and invalidateByTag all behave as the delegate makes them behave.

What changes is that the delegate's own text search, if it has one, no longer sees words. No store in this repository has one, since they all find entries by vector, but a backend somebody adds that indexes the prompt column for text search will index ciphertext.

The embedding is stored as it was. See EntryCipher for why, and for what it means.

Constructors

Link copied to clipboard
constructor(delegate: CacheStore, cipher: EntryCipher)

Functions

Link copied to clipboard
open suspend override fun clear(scope: String? = null)

Removes every entry in scope, or the whole store when scope is null.

Link copied to clipboard
open suspend override fun invalidateByTag(tag: String, scope: String? = null): Int

Removes every entry in scope carrying tag, or across the whole store when scope is null.

Link copied to clipboard
open suspend override fun put(entry: CacheEntry)

Writes entry, replacing any existing entry with the same CacheEntry.id.

Link copied to clipboard
open suspend override fun remove(id: String): Boolean

Removes the entry id. Returns true if an entry was actually removed.

Link copied to clipboard
open suspend override fun search(scope: String, embedding: FloatArray, limit: Int): List<ScoredEntry>

Returns up to limit entries from scope closest to embedding, best first.

Link copied to clipboard
open suspend override fun size(scope: String? = null): Int

Number of live entries in scope, or in the whole store when scope is null.

Link copied to clipboard
open suspend override fun touch(id: String)

Signals that the entry id was actually served to a caller.