Security

Isolation is a shape the code takes.

Five layers, each of which fails closed. The design goal is not that every developer remembers the rule. It is that forgetting the rule does not produce a working program that leaks. Below is what each layer actually enforces.

5layers, all fail-closed
0tenant IDs read from request bodies
1wrapper on every tool result
CIcross-tenant corpus per push
  1. EdgeNothing arrives trusted.

    A strict content security policy and sanitisation at the boundary, with a WAF in front of the public gateway.

    Markup that reaches a browser has already been through DOMPurify; text that reaches a model has already been wrapped as untrusted.

    CSP · WAF · sanitise
  2. GatewayIdentity is proven here, not assumed.

    Every request carries a JWT verified against the issuer's keys before anything else runs.

    Rate limiting is a per-user token bucket, and request shape is validated at the door rather than deep inside a handler where a missed branch becomes a bug.

    JWT · rate-limit · validate
  3. IdentityThe tenant cannot be spoofed by a field.

    Tenant identity travels between services as gRPC metadata and is never read out of a request body.

    A handler that forgets to scope a query does not quietly serve someone else's data. It fails to satisfy the call contract.

    metadata-only · fail-closed
  4. BrainUntrusted text stays untrusted.

    User input passes a prompt-injection interceptor, and every tool result (a web page, an MCP response, an uploaded file) is wrapped before it can influence the model.

    MCP servers are consented per tool, not per server. Provider secrets live in an AES-256-GCM vault.

    injection guard · wrap · vault
  5. DataThe filter is not optional.

    The Rust core enforces a mandatory tenant filter on vector search, fail-closed, and a cross-tenant corpus in CI proves the isolation on every push.

    This is the layer where a mistake would be invisible, so it is the layer with a test that fails loudly.

    tenant filter · CI-proven
Three things that are mechanism, not policy

A rule you can forget is not a control.

01

Entitlements are settled against a verified identity

Plan and remaining quota are read from the database on every gated action, keyed to the verified subject on the token. That read is the decision, and it is the only input to it.

Usage is metered into an append-only ledger with idempotency keys, so a replayed request cannot double-charge or double-grant.

02

Tool results are quarantined before the model reads them

A tool result is content from somewhere else: a web page, an MCP server, a file someone uploaded.

Every one of them goes through the same wrapper on the way back into the loop. There is no fast path that skips it for trusted-looking sources, because trusted-looking is exactly the attack.

03

Outbound fetches are guarded, not merely configured

The fetch service renders pages headlessly and returns markdown behind an SSRF guard, and MCP base URLs pass the same check.

The slice is deliberately narrow. There is no click-and-type browser fleet, because that is a security surface decision, not a backlog item.

BoundNone of this makes a self-hosted deployment secure by itself. You still own your TLS, your secrets at rest, your database backups and whoever you hand tokens to. Verity's claim is narrower and checkable: within the application, cross-tenant access fails closed and is tested on every push.

Read the layer you doubt.

The security model is in the repository along with the tests that hold it up. Start with the tenant filter and the cross-tenant CI corpus.