Skip to content

Security Model

Umi’s security model has one organizing principle: every effect that could harm the user pauses for explicit consent before executing. This is enforced structurally, not by convention.


umi-security is the single human-in-the-loop boundary. It is the first crate in the dependency graph that has any policy, and everything above it routes through it.

The gate has two parts:

decide(capability, granted, tainted) → Decision — a pure, synchronous policy function. It takes what the caller wants to do (capability), what the runtime has granted (granted), and whether the input is tainted (came from an untrusted source), and returns Allow, Deny, or RequireConsent. No async, no side effects — it is independently testable.

ConsentGate — an async handler that implements the RequireConsent case. It sends a typed GateKind prompt (e.g. ToolInvocation, EgressRequest, FileWrite) to the frontend and waits for the user to approve or deny. Fail-closed: a missing handler denies.


Capabilities are defined in umi-primitives. They are:

  • Additive — a caller can request only a subset of what has been granted; it cannot escalate.
  • HierarchicalCapability::All grants everything; narrower capabilities (Read, Write, Embed, Egress, Execute) carve out subsets.
  • Checked at invocation — every tool call, every MCP server invoke, every file write goes through decide() before executing.

The security invariant: no capability can be used without passing the gate. There is no back-door path.


Taint marks content that came from an untrusted source (a model response, an ingested file, an external tool result). It propagates through the data graph:

  • A model-generated tool argument is always tainted: true.
  • An ingested file from a channel source carries Provenance::Channel taint.
  • Taint is inherited: if a node in the graph is tainted, its outgoing edges carry that taint.
  • The gate treats tainted + effect = RequireConsent. The user sees the tainted content before it acts.

This is the defense against prompt injection: even if a model-generated argument contains an injected command, it arrives at the gate as tainted and pauses for approval before any effect fires.


Every piece of data in Umi carries a Provenance tag (Local, Web, Channel, Model). Provenance is set at the point of ingestion and propagated through every transformation. It is the basis for:

  • Taint decisions (non-Local provenance = potentially tainted)
  • Egress gating (content with Model or Channel provenance heading outward triggers consent)
  • Audit trails (you can always trace where a piece of data came from)

MCP tool servers are the primary expansion point for Umi’s capabilities — and the primary attack surface. The MCP security SoK (arXiv:2512.08290) identifies tool poisoning and prompt injection as the central risks.

Umi’s defense is structural: umi-agent::ToolRegistry routes every tool invocation — built-in or MCP — through umi-security. McpClient has no way to bypass the gate. The ToolGuard on every call carries the originating provenance so the gate knows whether the invocation came from a user action or a model-generated tool call.

This means a malicious MCP server that injects a tool-use payload into the model’s context will still hit the consent gate before any effect executes.


All persistent data is encrypted at rest using a passphrase-derived key:

  • Key derivation: Argon2id (OWASP 2024 recommended params) stretches the user’s passphrase into a Key Encryption Key (KEK).
  • Blob sealing: XChaCha20-Poly1305 with a random nonce per blob. The KEK seals the master key; the master key seals blobs.
  • Zeroize on drop: sensitive key material is zeroed from memory when it goes out of scope.

The vault is unlocked once at app start; the passphrase is never stored. Relaunching the app requires re-entering the passphrase.