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.
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.
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.
| 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 |
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.
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.
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 pluginThis 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.
- 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.
MIT