Security Model
Security Model
Section titled “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.
The consent gate (umi-security)
Section titled “The consent gate (umi-security)”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.
Capability model
Section titled “Capability model”Capabilities are defined in umi-primitives. They are:
- Additive — a caller can request only a subset of what has been granted; it cannot escalate.
- Hierarchical —
Capability::Allgrants 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 propagation
Section titled “Taint propagation”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::Channeltaint. - 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.
Provenance
Section titled “Provenance”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-
Localprovenance = potentially tainted) - Egress gating (content with
ModelorChannelprovenance heading outward triggers consent) - Audit trails (you can always trace where a piece of data came from)
MCP security boundary
Section titled “MCP security boundary”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.
At-rest encryption (umi-crypto)
Section titled “At-rest encryption (umi-crypto)”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.