# Iris **An AI operator that never clocks out.** Message it on Slack, Telegram, or its built-in web UI and it runs commands, writes its own skills on the fly, provisions infrastructure, and spins up a fleet of specialized sub-agents — each one optionally sealed inside its own Firecracker microVM — to get the work done. [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![CI](https://github.com/irisworks/iris-core/actions/workflows/ci.yml/badge.svg)](https://github.com/irisworks/iris-core/actions/workflows/ci.yml) [![Runtime version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Firisworks%2Firis-core%2Fmain%2Firis-runtime%2Fpackage.json&query=%24.version&label=runtime)](iris-runtime/CHANGELOG.md) [![Docs](https://img.shields.io/badge/docs-irisflow.dev-blue)](https://irisflow.dev/docs) One `curl | bash` on any Linux box turns it into a self-hosted, self-healing AI teammate — no cloud account, no Kubernetes, no vendor lock-in required. - **Self-extending** — Iris writes and hot-reloads her own skills; no redeploy to teach her something new - **Fleet, not chatbot** — spins up specialized sub-agents on demand, each talking over an HTTP bridge - **Defense in depth, opt-in** — Docker by default; flip a flag and every sub-agent runs in its own Firecracker microVM with a hardware KVM boundary - **Provider-agnostic** — Anthropic, OpenAI, Azure AI Foundry, or AWS Bedrock, switchable via env vars - **Three transports, one engine** — Slack, Telegram, and an optional built-in web UI (thread sidebar, live tool-call cards, attachments); adding a transport requires zero engine edits - **MCP servers** — external toolsets (stdio or remote HTTP) plug in via `meta/mcp.json`, hot-reloaded and manageable through chat (see `docs/mcp.md`) - **Resilient** — LLM retry with backoff, automatic context compaction, self-healing escalation - **Durable by design** — GitHub is the source of truth; the machine itself is cattle, not a pet - **Zero cloud dependencies to start** — secrets in `/iris/.env`, sub-agents in Docker; Azure Key Vault and Terraform are opt-in hardening, not requirements - **Encrypted secret store by default** — credentials live in an encrypted local store (`IRIS_SECRETS_MODE=store`), or a separate-uid injection proxy (`proxy`), with `env` kept as a legacy opt-out — plus one-time drop links so users never have to paste a secret into chat (see `docs/secrets.md`) ## Quickstart One command on any Ubuntu 22.04 machine — no Azure, no KVM: ```bash curl -fsSL https://raw.githubusercontent.com/irisworks/iris-core/main/install.sh | bash ``` This clones the repo to `/iris/repo` and runs the interactive bootstrap: it installs dependencies, asks for your LLM API key, walks you through creating a Slack app and/or Telegram bot (exact scopes shown in-terminal), builds the runtime, and starts Iris as a systemd service. The installer itself always runs from `main`, but it pins the cloned repo to the **latest release tag** (not `main`) by default, so you get a stable, released version rather than whatever merged an hour ago. Override with `IRIS_CORE_REF=main` (or any branch/tag) for development installs: ```bash curl -fsSL https://raw.githubusercontent.com/irisworks/iris-core/main/install.sh | IRIS_CORE_REF=main bash ``` **Verify:** `sudo systemctl status iris`, then in Slack: `@iris what model are you?` ![Iris writes herself a markdown-to-PDF skill, then uses it](assets/demo-md-to-pdf.gif) *Asked to build a markdown→PDF workflow, Iris checks that `pandoc` is available, writes herself a `convert.py` skill for it — then, when a user later hands her a markdown file, just uses the skill she wrote to convert it. No redeploy, no PR, no human in the loop.* To have Iris commit skills like this one back to GitHub (her long-term memory — the VM is ephemeral), answer `Y` to the GitHub token prompt during bootstrap and give it a repo to push to: your own private overlay repo, or a private mirror of `iris-core` (see [Company-Specific Extensions](#company-specific-extensions-overlay)). Never the upstream you cloned from, and never a public fork — this repo holds Iris's memory. Bootstrap writes this as `IRIS_GITHUB_ORG` / `IRIS_GITHUB_REPO` in `/iris/.env`. Want secrets in Azure Key Vault, or every command isolated in a Firecracker microVM? Pass bootstrap flags through the installer — see **[docs/SETUP.md](docs/SETUP.md)** for full walkthroughs of all four paths, including Telegram bot claiming: | | No Firecracker | With Firecracker | |---|---|---| | **No Azure** | *(default)* | `\| bash -s -- --setup --no-keyvault --firecracker` | | **Azure Key Vault** | `\| bash -s -- --setup --keyvault` | `\| bash -s -- --setup --keyvault --firecracker` | Already cloned? `bash bootstrap.sh --setup --no-keyvault` does the same without the installer. ## Talking to Iris - **Slack:** `@iris ` in a channel, or DM her directly. Attachments work both ways. - **Telegram:** message your claimed bot; groups and topic threads supported. - **Web UI:** set `IRIS_WEBUI_PORT` and open it in a browser — thread sidebar, agent picker, live tool-call cards, file attachments, and Stop/Compact/Reset buttons. Off by default; see [docs/web-ui.md](docs/web-ui.md). - **Control commands:** `stop`, `compact`, `reset` in an admin-mode Slack DM; `/stop`, `/compact`, `/reset` on Telegram; buttons in the web UI. - **Verbose tool output:** off by default — a run shows one status line that updates in place instead of a per-tool-call/thinking dump. Toggle with `verbose on|off|status` (Slack, any DM/mention) or `/verbose on|off|status` (Telegram); see [Channel Modes](docs/channel-modes.md#verbose-tool-output). ## Channel Modes How Iris behaves in a Slack channel is configured per channel in `/meta/channels.json`: | Mode | Behavior | |---|---| | `dm` | Default. Responds in DMs; channels need an `@iris` mention | | `admin` | Like `dm`, plus `stop` / `compact` / `reset` control commands | | `thread` | Only responds inside registered session threads (sessions created via the API) | | `interactive-thread` | Top-level `@iris` mention opens a session; replies in that thread continue it without further mentions | | `leads` | Every top-level message triggers Iris — no mention needed | | `passthrough` | Messages are forwarded to an external HTTP endpoint; the reply is posted back. Iris's LLM never runs | ```json { "C0XXXXXXX": { "mode": "interactive-thread", "requireMentionForTopLevel": true }, "C0YYYYYYY": { "mode": "passthrough", "url": "https://example.com/webhook", "secretName": "MY-WEBHOOK-KEY", "payload": { "sender_id": "{{sender_id}}", "message_text": "{{text}}" }, "replyPrefix": "*Bot:* " }, "D*": { "mode": "admin" } } ``` Keys are channel IDs (prefix wildcards like `D*` supported). Passthrough `payload` is a JSON template — string values may use `{{text}}`, `{{user_id}}`, `{{user_name}}`, `{{user_handle}}`, `{{sender_id}}`, `{{channel}}`, `{{ts}}`. The API key comes from `secretName` (via the `get-secret` skill) or the `PASSTHROUGH_API_KEY` env var. ## Configuration Set in `/iris/.env` (written by bootstrap) or as CLI flags (`--provider`, `--model`, `--sandbox`, `--transport`, `--api-port`). Flags override env. | Variable | Default | Purpose | |---|---|---| | `IRIS_PROVIDER` / `IRIS_MODEL` | `anthropic` / provider default | LLM provider and model (see `data/models.json`) | | `IRIS_SLACK_APP_TOKEN` / `IRIS_SLACK_BOT_TOKEN` | — | Slack tokens; presence enables the Slack transport | | `TELEGRAM_BOT_TOKEN` | — | Telegram token; presence enables the Telegram transport | | `IRIS_ENV` | `prod` | `preview` \| `prod` | | `IRIS_API_PORT` / `IRIS_API_HOST` | `3000` / `127.0.0.1` | Internal HTTP API bind (always on) | | `IRIS_API_TOKEN` | — | When set, API requires `Authorization: Bearer ` (except `/health`) | | `IRIS_BRIDGE_PORT` / `IRIS_BRIDGE_HOST` | — / `127.0.0.1` | Sub-agent bridge server (sub-agents only) | | `IRIS_LLM_TIMEOUT_SECS` | `90` | Per-attempt LLM timeout | | `IRIS_LLM_MAX_RETRIES` / `IRIS_LLM_RETRY_BASE_MS` | `3` / `2000` | Retry with exponential backoff on 429/timeout/transient errors | | `IRIS_COMPACT_THRESHOLD` / `IRIS_COMPACT_TARGET` | `0.7` / `0.1` | Pre-run auto-compaction trigger/target (fraction of context window) | | `IRIS_SLACK_MAX_CHARS` | `30000` | Safe Slack message length before splitting | | `IRIS_TELEGRAM_FORCE_RECLAIM` | — | Set `true` + restart to transfer bot ownership | | `IRIS_VERBOSE_TOOLS` | — (quiet) | Default verbose tool-call/thinking output on Slack/Telegram; overridable per channel with `verbose on/off/status` (Slack) or `/verbose on/off/status` (Telegram) | | `IRIS_WEBUI_PORT` / `IRIS_WEBUI_PASSWORD` | — (off) / — | Enable the built-in web UI on this port; shared-secret login (set it before exposing beyond loopback) | | `IRIS_SECRETS_MODE` | `store` | `store` (default) \| `proxy` \| `env` (legacy opt-out) — encrypted store / injection-proxy credential backend (see `docs/secrets.md`) | | `IRIS_SECRET_BROKER_URL` / `IRIS_SECRET_BROKER_TOKEN` | — | External secret broker for `GET /secrets/:name` (the bundled iris-broker in `proxy` mode, Vault, Infisical, or any HTTP service speaking the contract); default backend is env vars, then Key Vault if `IRIS_KEY_VAULT` is set | | `IRIS_GITHUB_ORG` / `IRIS_GITHUB_REPO` | — | The repo Iris commits her own skills/sub-agents/self-edits to (see [Company-Specific Extensions](#company-specific-extensions-overlay)) — your own private overlay or a private mirror of `iris-core`; never the upstream you cloned from, and never a public fork. Prompted by bootstrap alongside the GitHub token | | `IRIS_KEY_VAULT` | — | Azure Key Vault name (Key Vault profile only) | | `IRIS_BASE_DOMAIN` / `IRIS_EMAIL_FROM` | — | Public serving domain / outbound email sender | | `LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` | — (off) | Presence enables Langfuse tracing — one session-correlated trace per turn, with per-message token/cost and a `TOOL` observation per tool call. Tune with `LANGFUSE_HOST` (default `https://cloud.langfuse.com`), `LANGFUSE_ENVIRONMENT`, `LANGFUSE_RELEASE`, `LANGFUSE_TIMEOUT_MS`, `LANGFUSE_ENABLED=false`. Traces carry prompts, replies, and tool arguments/results — set `LANGFUSE_CAPTURE_IO=false` for cost/token telemetry only (see `docs/observability.md`) | > **Security note:** the internal API binds to loopback by default. If sub-agent > containers reach Iris via the Docker gateway (`172.18.0.1:3000`), set > `IRIS_API_HOST=0.0.0.0` **and** `IRIS_API_TOKEN` — never expose the API beyond > loopback without a token. ## Sandboxing Iris's bash tool executes at one of five isolation levels (`--sandbox`): | Mode | Flag | Use case | |---|---|---| | Host | `--sandbox=host` | Iris herself — trusted ops, full access | | Bubblewrap | `--sandbox=bwrap` | Per-channel filesystem isolation on one host, no daemon | | Docker | `--sandbox=docker:` | Containerized sub-agents | | Static Firecracker | `--sandbox=firecracker:` | Persistent sub-agent at a fixed IP | | Dynamic pool | `--sandbox=firecracker-pool` | Fresh microVM per channel, auto-destroyed after 30 min idle | Each microVM is defended in depth: KVM hardware boundary → minimal Firecracker VMM → jailer (chroot, uid 10000, seccomp) → per-VM `/30` TAP network → ephemeral rootfs destroyed with the VM. ## Skills A skill is a directory with a `SKILL.md` (YAML frontmatter: `name`, `description`) plus any scripts it needs. Workspace-level skills live in `/skills/` (symlinked to this repo's `skills/` for hot reload); per-channel skills in `/skills/` override workspace skills on name collision. Edits apply without a restart. Core ships platform skills only — things Iris needs to operate, extend, and heal herself (`spawn-agent`, `self-heal`, `self-extend`, `get-secret`, `github`, `send-email`, `serve-public`, `schedule`, `status`, ...). Domain and business skills belong in your install's overlay (see below). ## Sub-agents and the Internal API Sub-agents are separate runtime instances in Docker/Firecracker, registered in `agents.json` and reachable through an HTTP bridge (`@agentname` routing). Scaffolds live in `agents/`. The runtime exposes an internal HTTP API (default `127.0.0.1:3000`) for events, escalations, and session management — create sessions, inject messages, read history, reset context. Endpoint list in [`iris-runtime/src/engine/api.ts`](iris-runtime/src/engine/api.ts). Secrets are agent-scoped: sub-agents resolve them via `GET /secrets/:name` and must be allow-listed per secret in their `agents.json` entry (`"secrets": [...]`); caller identity comes from the authenticating API token, not a self-reported header. ## Company-Specific Extensions (Overlay) Link core, don't fork it — unless you need to change core's own code. Create a private repo with core as a submodule and your own agents/skills/config as an overlay: ```bash gh repo create iris-yourcompany --private cd iris-yourcompany git submodule add https://github.com/irisworks/iris-core.git core mkdir -p overlay/{agents,skills,data} ``` Add a `bootstrap-yourcompany.sh` wrapper that sets `REPO_DIR` before calling `core/bootstrap.sh`, then symlinks your overlay into the workspace. Pin `core` to a release tag and bump deliberately (see [docs/RELEASING.md](docs/RELEASING.md)). If you need to change core's own code — a new transport, say — fork to contribute it upstream, or take a **private mirror** if you can't publish it: iris-core is public, and GitHub forks of a public repo can't be made private. [docs/overlay.md](docs/overlay.md) compares the install shapes and covers both upgrade paths. ## Operating ```bash sudo systemctl status iris # health sudo journalctl -u iris -f # logs sudo systemctl restart iris # restart (state is on disk; nothing is lost) ``` If `start` silently does nothing, rebuild first: `cd /iris/repo/iris-runtime && npm install && npm run build` | Symptom | Likely cause | Fix | |---|---|---| | `iris.service` fails to start | Missing env vars | Check `/iris/.env` and `journalctl -u iris` | | `/dev/kvm` not found | VM series without KVM | On Azure, resize to Ddsv5 (e.g. `Standard_D4ds_v5`) | | `firecracker: permission denied` | Not in kvm group | `sudo usermod -aG kvm $USER`, re-login | | VM boots but `/health` times out | exec-server not started | `journalctl -u iris-fc-` | | Jailer fails to chroot | `irisjailer` user missing | `sudo groupadd -g 10000 irisjailer; sudo useradd -u 10000 -g 10000 -r -s /usr/sbin/nologin irisjailer` | | rootfs missing | Build script not run | `sudo bash scripts/build-firecracker-rootfs.sh` | ## Repository Layout ``` iris-core/ ├── install.sh # curl-able one-command installer (clone + bootstrap) ├── bootstrap.sh # interactive install / full rebuild ├── iris-runtime/ # @iris-core/runtime — the TypeScript engine ├── skills/ # platform skills (hot-reloaded) ├── agents/ # sub-agent scaffolds ├── scripts/ # Firecracker VM lifecycle ├── data/ # CONSTITUTION.md, MEMORY.md, models.json.template — LLM provider config ├── docs/ # rendered docs site — setup, configuration, channel modes, web UI, ... └── terraform/ # optional profile — dynamic Azure resources ``` ## Design Discussions Proposed features get a design doc under `docs/plans/` before any code lands — context, alternatives considered, and open questions, so the tradeoffs are visible before the implementation is. See [`docs/plans/2026-08-05-task-primitive-design.md`](docs/plans/2026-08-05-task-primitive-design.md) for an example. To weigh in on one, open a [GitHub Discussion](https://github.com/irisworks/iris-core/discussions) linking the doc rather than commenting on a PR — Discussions persist after a PR merges or closes, which is where the conversation actually needs to live for a proposal still being shaped. ## Releases Semver tags (`vX.Y.Z`); installs pin their `core` submodule to a tag. Changelog in [`iris-runtime/CHANGELOG.md`](iris-runtime/CHANGELOG.md), process in [`docs/RELEASING.md`](docs/RELEASING.md). ## License Apache License 2.0 — see [LICENSE](LICENSE) and [NOTICE](NOTICE).