Iris
Iris is a self-hosted personal AI assistant MCP server that manages calendar, tasks, daily planning, and memory over Telegram, with deterministic action gating and a security envelope.
README
Iris
A self-hosted personal AI assistant that runs my life over Telegram — calendar, tasks, daily planning, memory — built as a custom MCP server and hook-plugin suite on top of the open-source OpenClaw agent harness, with Claude as the intelligence.
The design bet: an assistant you trust with write access to your life cannot be a prompt. Everything that must never go wrong — action gating, priority ranking, conflict resolution, provenance, "what day is it" — is deterministic, unit-tested code. The model supplies judgment inside an envelope that code enforces.
Why it exists
Calendar, task app, and notes are three disconnected surfaces, each claiming to be the truth. Iris inverts that: a small git-backed "brain" (mission → goals → priorities) that I own is the single source of truth, and the calendar/task apps are projection surfaces rendered from it. The assistant reads all of it, ranks the day deterministically, delivers a morning digest, and acts — creating events, moving tasks, updating memory — behind approval gates that scale with blast radius.
It has been my daily driver since June 2026.
Architecture
flowchart LR
TG[Telegram]
subgraph BOX["Ubuntu box — Docker, Tailscale-only, no public ports"]
GW["OpenClaw gateway<br/>(rented: agent loop, channels, cron)"]
subgraph OWNED["Owned core — this repo"]
MCP["MCP server (src/)<br/>planner · brain · memory · taxonomy<br/>reconciliation · security envelope"]
PLUG["Hook plugins (plugins/)<br/>confirm-gate · verify-gate<br/>live-state · traits · prompt-capture"]
end
BRAIN[("Markdown brain + memory<br/>git-backed workspace")]
end
TG <--> GW
GW <-->|"tools"| MCP
GW -.->|"before_tool_call · prompt hooks"| PLUG
MCP <--> BRAIN
GW <--> GCAL["Google Calendar MCP<br/>(rented, OAuth scope-narrowed)"]
GW <--> TODO["Todoist MCP<br/>(rented, tool-allowlisted)"]
GW <--> ANTH["Anthropic API<br/>Haiku default (live) · /model escalation"]
Own the brain, rent the surfaces. The repo builds only the differentiator — planning, memory, authority, safety. The agent loop is rented (OpenClaw), the calendar and task surfaces are rented MCP servers pinned and narrowed to least privilege (OAuth scope patch on the calendar server; tool allowlists on both). The security envelope wraps the rented servers too.
Component map
| Path | Responsibility |
|---|---|
src/planner/ |
rank_day — pure, LLM-free day ranking: weighted-additive score + Eisenhower quadrant + time-block-aware plan |
src/reconciliation/ |
Authority engine: a brain-vs-live conflict is decided by the slot's type-authority prior (state→live surface, intent→brain), downgrading to flag-and-ask when the prior's loser is fresher; same-side conflicts use a recency → provenance → ask ladder. No model on the path |
src/taxonomy/ |
Action classifier: every tool call maps to autonomous / auto+notify / confirm-first / quarantine+confirm by reversibility + blast radius |
src/security-envelope/ |
Quarantine (untrusted-content sanitize + envelope), provenance tagging, Rule-of-Two invariant, web egress gate |
src/brain/, src/memory/ |
Git-backed Markdown SSOT: read tools + gated write tools (propose/commit_brain_edit, memory_promote/memory_forget) — every durable write is one git revert from undone |
src/traits/ |
Owner-tuneable personality dials (humor, verbosity, bluntness…) with a non-tuneable safety/honesty floor |
src/grounding/, src/projection/ |
Verify-before-act helpers; calendar-as-projection write path (tagged, idempotent by record id) |
plugins/confirm-gate/ |
before_tool_call hook → runs the taxonomy classifier → fail-closed Telegram approval via a text /approve <id> allow-once|deny command (300s timeout → deny); also hosts the web-egress gate |
plugins/verify-gate/ |
Hard-blocks destructive calendar and Todoist writes whose target wasn't read in the same turn |
plugins/live-state/ |
Injects fresh calendar/task state + an authoritative local Now: line every turn — time is an injected fact, never a model inference |
plugins/traits/, plugins/prompt-capture/ |
Persona rendering into the system prompt; wire-level capture feeding the eval harness |
scripts/eval/ |
Offline model-eval harness: wire-exact corpus, replay across providers, deterministic scorers, cross-family LLM judge |
deploy/ |
Hardened gateway config, compose, vendored patches (pinned + hash-checked) |
prs/, docs/ |
The development process: one research-gated PR file per change, architecture SSOT, design logs with the full decision trail |
The gated write path
flowchart TD
A["Proposed tool call"] --> V{"verify-gate:<br/>destructive write —<br/>target read this turn?"}
V -- "no" --> B["Blocked → model told to read first"]
V -- "yes / not destructive" --> T["taxonomy classifier<br/>(deterministic, unit-tested)"]
T --> C{"action class"}
C -- "autonomous /<br/>auto+notify" --> X["Execute"]
C -- "confirm-first /<br/>quarantine+confirm" --> P["Telegram approval — /approve command<br/>fail-closed, 300s timeout → deny"]
P -- "approve" --> X
P -- "deny / timeout" --> D["Denied"]
Governing invariant (Rule of Two): no single operation may combine reads untrusted content + accesses sensitive data + changes external state without a mandatory human gate. In practice it's enforced as a system rather than one choke-point function: session taint escalates every same-turn write after untrusted input, and the sensitive store is read through its own gated tool. Confirm-gate additionally runs a web-egress gate — a web_fetch to a domain this turn's searches never surfaced is confirm-first (kills the classic exfil-by-URL channel). Prompt injection is unsolved; the dangerous combination stays permanently gated no matter how good the model is.
Key design decisions
Each of these had a live alternative that was researched and rejected — the full trail is in docs/*/DESIGN-log.md and the prs/ files.
-
Deterministic gates, not prompt-based safety. System-prompt instructions like "verify before acting" were live-tested and proven insufficient — the assistant still moved real calendar events off a wrong-day inference. The fix each time was a code-level hook (
verify-gate,confirm-gate) that makes the failure structurally impossible rather than discouraged. Rejected: relying on model compliance. -
Logic in code wherever judgment isn't required. Ranking, conflict authority, action classification, and time grounding are pure functions with unit tests. Deterministic code beats model judgment over long context, and the system stays correct even with extended thinking off. The model handles what only a model can: natural-language policy, tie-breaks, conversation. Rejected: letting the LLM rank the day and arbitrate stale-vs-fresh conflicts.
-
Quarantine untrusted content at persistence time. Calendar invite notes and fetched web content are Unicode-stripped, wrapped in an explicit "data, not instructions" envelope, and provenance-tagged
untrusted-derivedbefore the planner ever sees them — a synchronous rewrite hook, not an async model pass — and the turn is tainted so same-turn writes escalate to gated approval. Nothing untrusted is ever durably written unguarded (anti-memory-poisoning; injection canaries live-verified quarantined). PDF attachments take a separate path: the runtime's bundled extractor inlines them inside OpenClaw's native untrusted-content framing (canary-verified live), not the owned pipeline. Rejected: dual-LLM token isolation — the runtime's persist hook is synchronous, so it's deferred, not dismissed. -
Model selection by measurement, not vibes.
scripts/eval/replays the assistant's own captured traffic offline against candidate models with deterministic scorers and a cross-family judge. The capture path is wire-exact (billed request body reproduced to ±0 tokens) after the naive approach — replaying reconstructed trajectories — was tested and refuted (−18.8% token fidelity). The harness's verdict set the current default (Haiku, −67% cost/turn at parity for routine traffic, with per-conversation escalation to Sonnet/Opus). It also halted a shipped decision: per-turn cross-provider routing was reverted when the live trial showed a stateful turn ships its whole session history to whichever provider serves it — the privacy carve-out unit is the session, not the turn. Rejected: benchmark-driven model choice, and the cross-provider router it initially justified. -
Time is an injected fact. A real incident (assistant asserted the wrong weekday, concluded the calendar UI was buggy, and moved real events) traced to a context where every ambient timestamp was UTC and no authoritative local "now" existed. The fix: a hook injects
Now: <local datetime> — AUTHORITATIVEplus weekday annotations on every date, each turn, cache-neutrally. The incident class is closed structurally. Rejected: prompt rules telling the model to be careful about timezones.
Testing
294 tests across 22 files (vitest), verified passing from a clean clone. Coverage follows the "logic in code" rule — the deterministic pieces are the tested pieces:
- ranking math, taxonomy classification, reconciliation authority, provenance parse/render, quarantine sanitization, egress-gate policy, eval-harness scorers/corpus logic
- adversarial fixtures: prompt-injection and memory-poisoning payloads assert the gates fire and nothing durable is written from untrusted content
- beyond unit tests, every PR in
prs/records its live binding test — the deployed behavior verified over Telegram against the real gateway before the PR closes
npm ci
npm test # 294 tests
npm run typecheck
npm run build # server bundle; build:plugin:* for each plugin
Running it
This is a personal, single-user system — the repo is published as a working reference, not a turnkey product. A full deployment needs an OpenClaw gateway (Docker), a Telegram bot, Google Calendar OAuth, a Todoist token, and an Anthropic API key; deploy/ contains the hardened gateway config, compose file, vendored patches, and a README with the wiring. One drift note: the committed gateway config predates the Haiku-default model switch (PR-033, applied on the live box) — it still pins Sonnet as primary. Expect to adapt paths and secrets handling to your own box.
Status — honest scope
- In daily use since June 2026; v0.2.x. The commit history is the real changelog — each change is a research-gated PR with verification criteria.
- The agent loop, channels, and cron are OpenClaw (third-party). This repo is the MCP server, plugins, eval harness, and deployment hardening on top.
- Proactivity is deliberately minimal (one daily digest); the interrupt engine is designed but deferred.
- Scanned/image-only PDFs don't reach the turn (documented gap); finance integration is designed for a sidecar-isolation pattern but not built.
- Two vendored runtime patches (thinking-block handling, wire capture) are pinned to the current OpenClaw release and retire when upstream ships equivalents.
License
MIT
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.