MoriMori
Mori

Security & approvals

Mori runs on your machine with your keys, and every risky action stops for your yes.

The security model

Mori is local-first. The agent, its tools, its memory, and its audit trail all live on your machine; the only outbound traffic is the model calls made with API keys you supply. There is no Mori cloud in the execution path, so there is no server-side actor that can run a tool on your behalf.

Every action passes through layered gates before it touches anything real:

  • Permission levels decide which tool categories exist at all for the current session.
  • Per-call permission checks run on every single tool invocation, not just at startup.
  • Typed approvals stop irreversible actions — purchases, remote confirmations — until you explicitly say yes.
  • Checkpoints snapshot files before Mori overwrites or deletes them.
  • Post-turn verification re-checks Mori's claims against reality before you read them.
  • A durable audit log records every tool call, every approval, and every denial.

Permission levels

Tool access is governed by isToolPermitted, which runs before every tool call. If agent mode is off, no tool is permitted, full stop. With agent mode on, the permissionLevel setting selects one of three tiers:

  • safe — browser tools and read-only tools only. MCP tools are blocked entirely at this level.
  • standard — the default. Most categories are allowed, with per-category kill switches: allowFileWriting, allowTerminalCommands, allowBrowserControl, allowAppLaunching. Raw keyboard simulation (type_text, press_keys, hotkey) is off unless you opt in with allowKeyboardControl — app-targeted typing is allowed, but blind keystrokes into whatever has focus are not.
  • full — everything the category toggles would otherwise gate.

Read-only tools (read_file, list_files, git_status, search_history, and the rest of the verification set) are always permitted when agent mode is on — Mori can always check reality, even when it can't change it. A denied call doesn't fail silently: the denial is written to the action log with the exact reason, e.g. Permission denied: run_command is not allowed under current permission level (safe).

Typed approvals on remote surfaces

When you drive Mori from Discord, Telegram, or the phone app, confirmations are not a loose "reply yes" convention. Every approval request is a typed object with its own ID and a hard expiry:

  • Each request gets a unique approval ID and a two-minute TTL. The prompt tells you both: "approval apr_xxx — expires in 2 minutes; anything but a clear yes is a no."
  • Only an unambiguous yes approves. An unrecognized reply is treated as a deny, and Mori tells you so. Silence past the TTL expires the request — also a deny. The system fails closed in every branch.
  • Every decision — approved, denied, unrecognized-treated-as-deny, expired — is written to the durable audit log with the approval ID, the request text, and which platform answered.

An explicit "stop" or "cancel" from you on any surface halts the running task through the same machinery as the desktop Stop button, and that cancellation is logged too.

Purchases: Mori never clicks the pay button

Spending money is gated by button detection, not model judgment. In the browser agent, when a click or submit targets an element whose label matches the pay-button pattern (place order, pay now, buy now, and so on), the click is intercepted before it happens. Mori screenshots the review screen, raises onPendingPurchase with a summary, the exact button label, and the image, and ends its turn. The same gate fires if the model declares itself done while a pay button is present on the page.

The order is placed only when you explicitly approve — the confirm button in the app, or a clear yes to a typed approval on a remote surface. That approval calls confirmPlaceOrder, which re-reads the review page, re-locates the pay button by its exact text, clicks it once, waits, and screenshots the result so you see the confirmation page. If the button is gone, it refuses and tells you to reopen checkout rather than guessing.

The computer-use loop has the same rule enforced twice: a hard stop that refuses to execute any click or keystroke the model narrates as a final purchase or trade, and — in the default ask autonomy mode — a per-action confirmation gate on every click, keystroke, and key press. Card details, credentials, and the final spend tap are yours, always.

Post-turn verification and the honesty check

Mori's reply text is not treated as ground truth. After any turn that changed things, the post-turn verifier re-checks each successful mutation against reality: files it claims to have created are probed on disk, folders are listed, deletions are confirmed absent, and dev servers it says it started are probed over HTTP. The result is appended to the reply as a verdict line — ✓ Verified: 3/3 changes confirmed on disk — or a warning naming exactly what could not be verified. Verification is capped and time-boxed so it can never balloon a turn, and a pure-chat turn gets no verdict line at all.

A separate honesty check targets the highest-stakes claim: "I sent it." If the reply asserts an email or message went out but no send-capable tool actually succeeded that turn, Mori appends a correction stating that nothing was sent and the claim above is wrong. A fabricated "done" does not stand.

Checkpoints before every write

Before Mori overwrites or deletes a file, the prior bytes are copied into a per-file checkpoint history under the app's data directory. Snapshots are capped at ten per file and skip anything over 10 MB, so the history never bloats — and the snapshot step can never block or fail a write.

Three tools ride on this: list_checkpoints shows what's saved for a path, rollback_file restores the most recent snapshot, and search_history finds prior states. When Mori deletes a file it says so in the result: "a checkpoint was saved — rollback_file can restore it." Destructive file operations are undoable by design.

The MCP server fails closed

Mori can act as an MCP server so other agents — Claude Code, IDEs, scripts — can use it as a tool. That surface is deliberately minimal:

  • Disabled by default. You turn it on in Settings.
  • Loopback only. It binds 127.0.0.1:4519 and nothing else — never a network interface.
  • Bearer token on every request. The token is generated once, shown in Settings, and any request without it gets a 401.
  • Reads only. The v1 surface is status, file reads, file listing, file search, and board/goal reads. No terminal, no file writes, no sends, no computer control.
  • Task creation doesn't execute. mori_create_task lands a card in your board's INBOX — a human triages it before any worker touches it.

Each POST is handled by a fresh stateless server-transport pair, so nothing leaks between clients.

curl -X POST http://127.0.0.1:4519/mcp \
  -H "Authorization: Bearer <token from Settings>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Stranger isolation and the audit log

On Discord and Telegram, ownership is bound to your immutable platform account ID — captured once, never matched by display name — so impersonation by nickname is impossible. The phone app is owner-by-construction: it is signed into your account.

Anyone who is not the verified owner gets a hard-isolated experience. Strangers receive plain conversation from the model's own knowledge only: no tools, no screen, no files, no terminal, and no machine actions of any kind. Your saved memory is stripped from the prompt entirely for those chats (setSuppressMemory), so it cannot be socially engineered out of the model, and verification output from your concurrent tasks is never leaked into a stranger's reply. Their messages are treated as untrusted data — instructions to ignore rules, act on the machine, or reveal owner information are declined.

Underneath all of it sits the audit log: every completed tool call is appended to a per-profile audit.jsonl with timestamp, tool, argument summary, result, and status, alongside every approval decision. Auditing is best-effort by contract — it never blocks or breaks a task — but the record is durable, local, and yours to review or clear.