evo-ai — Security in local mode

Your data never leaves the server.

How evo-ai keeps a customer's data private when run in fully local (private-hosting) mode: what the guarantee is, the mechanisms that enforce it, and an honest account of the current status.

The two guarantees, kept distinct

Security promises about AI systems get muddled when two different guarantees are conflated. evo-ai treats them separately:

  • Data residency — "your data never leaves the server." No customer data crosses the network boundary to any third party.
  • Grounding — "answers come only from your records." The assistant answers from the customer's own data, never from a model's opaque world knowledge.

Local mode is what makes the residency guarantee absolute. Grounding holds in every mode. This page is primarily about residency.

What "local mode" means

In local mode, every component that touches customer data runs on the same host (or private network) the customer controls:

  • LLM (answers + routing) — Ollama running a local model (e.g. Llama 3.x); inference is on-box.
  • Embeddingsfastembed (ONNX) in-process, on CPU; no embedding API is called.
  • Vector store — Qdrant on the same host.
  • Config + run history — a SQLite file on the host.
  • Data connectors — read-only DB / local sources, within the trust boundary.

There is no step in the query pipeline that makes an outbound call with customer data when the model and embeddings are local. A question, its retrieved context, and its answer all stay on the box.

The design rule that keeps it true

Every enrichment or feature must be executable fully on-box, or it doesn't ship for local mode. Embeddings run in-process, not via a hosted API; any reference data is bundled with the deployment and consulted in-process, never fetched at query time; and no feature geocodes, enriches, or validates by calling an external service. This rule is what lets the residency guarantee survive the addition of new features over time.

Tenant isolation (defence in depth)

A single deployment can serve many customers, so isolation is enforced at multiple independent layers — a bug in one does not collapse the others:

  • Identity from verified claims only. tenant_id is read from a cryptographically verified JWT (RS256, verified against the identity provider's JWKS), never from the request body. A caller cannot assert a tenant they weren't issued.
  • Physical vector separation. Each (tenant, collection) gets its own physical Qdrant collection — plus a tenant_id metadata filter applied to every query. Two independent mechanisms must both fail to cross tenants.
  • Server-bound SQL scope. For the analytics path, tenant-scoped views bind the tenant id server-side as a parameter. The LLM-generated SQL is wrapped in these views; it has no way to widen the scope.

The text-to-SQL boundary

Letting a model write SQL is powerful and dangerous, so the analytics path is locked down independently of anything the model produces:

  • Read-only role. A SELECT-only role with default_transaction_read_only = on and a statement timeout — the model literally cannot write, even if it tried.
  • Empty search_path. Bare table names resolve to nothing; only the curated tenant-scoped views are reachable.
  • Static validation before execution. Single-statement SELECT only; stacked statements, comments, schema-qualified escapes (app., public., pg_*, information_schema), and write/DDL keywords are rejected.
  • Row cap + per-request view allow-list. Results are capped; a request restricted to certain datasets can only reference those views, and any reference outside the allowed set is rejected before execution.
  • Fail-safe fallback. Any planning, validation, or execution failure falls back to vector retrieval and its relevance gate — a failure never degrades into an ungrounded answer.

Nothing is ever created in the source database — the "views" are query fragments prepended at read time, not objects installed in the customer's DB.

The write boundary (actions)

When the assistant helps change data (e.g. assigning a record), the security model is strict:

  • evo-ai never writes. It only ever returns a structured proposal, with names copied verbatim — it does not resolve identities or execute anything.
  • The application executes, under the asking user's identity, through the same code paths its normal UI uses — so permissions, history, and audit logging all apply exactly as they would for a manual change.
  • A human confirms. Nothing changes without an explicit confirmation, and ambiguous targets are disambiguated by the user, never guessed.
  • Provenance is recorded. The audit entry carries the acting user and a marker that the change was made via the assistant.

Because actions can only originate from what the user typed, content retrieved from documents can never trigger an action — closing the prompt-injection path to side effects.

Secrets and prompt-injection posture

  • Provider API keys (for cloud mode) are encrypted at rest with Fernet under a deployment SECRET_KEY, and are never echoed back by the API.
  • Service keys (for trusted application backends) are stored only as SHA-256 hashes; the plaintext is shown once at mint time and never retrievable again.
  • Connector credentials (DB DSNs, tokens) are encrypted at rest with the same scheme.

Retrieved content is treated strictly as data. The grounding policy instructs the model never to follow instructions found inside the context; the relevance gate refuses off-topic input before the model is even invoked; and retrieved content cannot initiate actions. These are verified by a guardrail regression suite — see Guardrails.

Honest current status

The local-mode architecture above is built and tested — the local model path (Ollama), in-process embeddings, on-box vector store, and every isolation and guardrail mechanism exist and are exercised by the test suites.

The current production deployment runs a cloud model as an interim, hardware-driven choice: the production box is not yet sized for a local model of sufficient quality, so query text (including retrieved context) currently reaches the cloud provider under the operator's own API key. This is a deliberate, disclosed trade-off, not a hidden one — and it is exactly the gap local mode closes. Every mechanism on this page except the model locality is already live in production; flipping to local mode is a hosting decision, not a re-architecture.

Back to the evo-ai overview, or see the multi-tenant platform and guardrails. Questions? Get in touch.