# beaver-agent My real [beaver-gateway](https://git.kotikot.com/beaver/beaver-gateway) setup: the Beavering Manager, a personal agent that lives on a home server, reads an Obsidian vault, talks through Telegram and deep chats in the vault, remembers people and promises, and has hands (Komodo, Home Assistant, T3 Code threads, calendars, Firefly, Telegram history through beavergram). The narrative and the docs are on [the site](https://beaver.kotikot.com/docs); this README is the operator's part: how to run it. Uses Claude Code and Raycast subscriptions for agents, has some MCPs set up. You can use this as-is or modify to match your needs: `config.py` assembles the gateway from the `beaver_agent/` package. It is opinion, not core - every piece is meant to be rewritten for your own vault. Seeding memory from an existing vault (old chats into digests, diary days and thoughts into notes about people) is one run of `scripts/seed_memory.py`; `--dry-run` first. ## Requirements - Docker + Docker Compose - Obsidian Sync (for `obsidian-headless`) - claude.ai sub - raycast sub - Firefly III and its PAT ## First launch ### 1. Create `.env` and `raycast.json` ```bash cp .env.example .env # every empty value is documented inline; secrets: openssl rand -hex 32 nvim .env # build raycast config on your mac (beta by default, consider checking raycast-api for instructions) raycast-api init # copy config.json to your deployment server docker compose up -d ``` ### 2. Set up Obsidian Sync ```bash docker exec -it beaver-obsidian ob login docker exec -it beaver-obsidian ob sync-setup --vault "yourvault" docker restart beaver-obsidian ``` Check: ```bash docker exec beaver-obsidian ls /vault ``` Only markdown is synced by default. To sync everything: ```bash docker exec beaver-obsidian ob sync-config \ --file-types image,audio,video,pdf,unsupported docker restart beaver-obsidian ``` ### 3. Set up claude On your mac run `claude setup-token` and put the result into `.env` as `CLAUDE_CODE_OAUTH_TOKEN`. That is the whole auth: no `/login` inside the container, no dialogs. The token draws from your subscription limits like regular Claude Code; rotate it by running `claude setup-token` again. Claude agents run on the Claude Agent SDK. Prompts are granules under `мета/бобер/промпты/` in the vault - plain markdown, the `` wrappers and the order live in `beaver_agent/prompts.py`; skills are the folders under `мета/бобер/скиллы/` (each becomes a plugin, keep `name:` in SKILL.md latin), which window loads which set is `beaver_agent/skills.py`. The vault must be synced before the gateway can start. The model process runs as `beaver-runner` with a whitelisted environment; the vault is mounted read-write (it is the dispatcher's home), and `beaver_agent/policy.py` keeps it out of `.obsidian`, `мета` outside `мета/бобер`, and from deleting anything but its own files. ### 4. Mint a token Open admin at `http://localhost:62990/` (or `https:///` if Caddy), sign in with `ADMIN_USER` / `ADMIN_PASS`, go to **Tokens → Create**, scope `*` for first run. ### 5. Smoke test ```bash docker exec beaver-gateway ls /vault | head docker logs beaver-gateway | grep -i "agent registered" curl http://localhost:62990/anthropic/v1/models \ -H "Authorization: Bearer " ``` ## Where to plug things in The admin dashboard renders ready-to-copy URLs and snippets for each of these Everything is on one port under its own path; Caddy forwards the whole domain as is. - any Anthropic client: `http://localhost:62990/anthropic` or `https:///anthropic`, model = agent name (`beaver-opus-high` etc) - MCP clients (Claude Desktop, Raycast extension): `/mcp//`, discovery page at `/mcp/` - Obsidian companion plugin: `/md` as the plugin's "Base URL", `/api` for the panel - voice points around the flat: `POST /voice/` with a key of scope `voice` (see below) - **Admin UI:** `http://localhost:62990/` or `https:///`, login from `ADMIN_USER` / `ADMIN_PASS` ## Voice points (`beaver_agent/voice/`) ESP32 boxes with microphones around the flat, built by a neighbour, post their own JSON and get an answer back: ```sh curl -X POST https:///voice/ -H "X-Api-Key: " -d '{ "room": "andreii", "device": "esp32-1", "request_id": "3fd0d6ed-...", "content": "ало клод как дела"}' {"request_id":"3fd0d6ed-...","room":"andreii","device":"esp32-1", "reply":"живу, слушаю","conversation":"…"} ``` `"stream": true` in the body answers `text/event-stream` instead: `delta` frames with pieces of the text, `done` with the same body as above. - **One branch for the whole flat**, titled 🎤 Голосовой, so Telegram gives it a topic under that name: what was asked and what was answered is readable there, and writing in that topic talks to the same agent. A cron at 04:05 starts the branch over in the same topic, the way the master rotates; a day nobody spoke in is left alone. - **Its own agent** (`beaver-voice`, haiku): `cwd=/tmp`, no tools, no MCP hands, no gateway tools, and a prompt that ships in this repo instead of the vault. A stranger may be standing at the speaker, so the agent has nothing to leak rather than instructions not to - and `Recall`/`ReplyLog` are muted for it, so the envelope never hands it pointers into the vault. - **Its own keys**: scope `voice` (Tokens → Create), which opens nothing else - not `/api`, not the panel, not the token page. ## Exposing to the internet `caddy/site.caddy` holds the routes (one `reverse_proxy`, domain from `BEAVER_DOMAIN`); on a shared server it is bind-mounted into that server's Caddy as `projects.d/beaver-agent/`, on its own box `caddy/docker-compose.yml` runs a Caddy around it. ```bash cd caddy cp Caddyfile.example Caddyfile cp .env.example .env # BEAVER_DOMAIN, BEAVER_UPSTREAM, Cloudflare token if any docker compose up -d ``` Set `PUBLIC_BASE_URL=https://` in beaver-agent's `.env` so advertised endpoints use the outside origin. ## Deploy: `main` is work, `stable` is production Komodo deploys only the `stable` ref; pushing `main` deploys nothing. Commit during the day, then ship when the master thread is idle: ```bash make deploy # git push origin main:stable ``` The gateway image is built from `beaver-gateway#${GATEWAY_REF}` at deploy time, so push the gateway first. `docker-compose.yml` has a healthcheck on `/healthz`; Komodo waits for it before it considers the deploy done. To drain first (архитектура §8.6), call the deploy hook instead of pushing by hand: it waits until no master turn is running, then asks Komodo: ```bash curl -X POST https:///hooks/deploy -H "Authorization: Bearer " ``` Needs `KOMODO_URL` / `KOMODO_KEY` / `KOMODO_SECRET` in `.env` (and `KOMODO_STACK`, default `beaver-agent`). Komodo alerts can be pointed at `POST /hooks/komodo` the same way - they land in the master as an urgent inject. Jobs, the queue and the subscription window are on the admin **Jobs** page. The same key backs the `komodo` tool the dispatcher and deep chats get (`beaver_agent/hands/komodo.py`): one MCP tool with an enumerated `action` - `status`, `stacks`, `containers`, `logs`, `search_logs`, `updates`, `update`, `deploy`, `restart`, `exec`. `exec` is `docker exec` into any container of the fleet except infrastructure ones (`exec_deny`, periphery by default); prune, destroy and a host terminal do not exist in the enumeration, so they cannot be asked for. The key never reaches the model process. ## Layout: `config.py` + `beaver_agent/` `config.py` is a page: it imports the pieces and calls `Gateway(...)`. The pieces live in `beaver_agent/`, one concern per module: - `vault.py` - paths and the agent's zone, `TZ` - `prompts.py` - how each role's system prompt is assembled from granules - `skills.py` - which window loads which skill set - `policy.py` - `PreToolUse` rules and vault zones (`ZONES`, `VAULT_POLICY`) - `hands/` - every MCP the setup has and which agent gets which one; the connectors (komodo, home assistant, vibegram) live here too - `agents.py` - the roles (dispatcher, deep, distiller, curator, triage, raycast) and the list of instances with models and effort - `frontends.py` - the windows: Telegram for master and branches, vault files for deep chats, `/api`, `/admin`, `/anthropic`, `/mcp`, `/voice` - `voice/` - the voice points: the window, the branch and its nightly rotation, the prompt the cheap agent runs on - `texts.py` - everything the gateway says to the model or to the user, in Russian (the gateway's own defaults are English) - `memory/` - the recall block under a message, the reply log, handouts and seeds, the vault watcher, the curator briefing - `jobs/` - crons and webhooks, one file per job ## Policy (`beaver_agent/policy.py`) `bypassPermissions` everywhere; the boundary is `PreToolUse` rules declared per agent (`ClaudeAgent.policy`): the vault is the dispatcher's home, so it may create, edit and move anywhere except `.obsidian` and `мета` outside `мета/бобер`, and may delete only its own files; the distiller and curator stay in `мета/бобер`; `rm`/`mv`/`cp`/`tee`/`sed -i`/redirects into the vault outside those zones are refused with a reason; `mcp__firefly__store_*` and `update_*` need the `firefly` skill opened first in the same session, and the master cannot open the `vault/` skills at all (a branch can). Every tool call lands in the admin **Audit** page as `tool_call`. Run `make check`. ## Editing the setup on the mac `make sync` installs `../beaver-gateway` editable (extra `local`), so imports resolve against the checkout you are working on; `uv sync --extra prod` pulls the gateway from git instead. Neither is used by the image - the container builds the gateway itself. To assemble the config against your local vault: `BEAVER_VAULT=~/path/to/vault uv run python -c 'from beaver_gateway import config; from pathlib import Path; config.load(Path("config.py"))'` with a `.env` next to it. ## Useful commands ```bash # shell into the gateway container (claude CLI, bun, python with venv all live here) docker exec -it beaver-gateway bash # what obsidian-sync has pulled docker exec beaver-obsidian ob status # force a one-off sync (don't wait for the continuous tick) docker exec beaver-obsidian ob sync # per-service logs docker compose logs -f gateway docker compose logs -f obsidian-headless # pull the latest beaver-gateway (when GATEWAY_REF=main) docker compose build gateway && docker compose up -d gateway # reload config.py / beaver_agent without a full rebuild docker compose restart gateway ``` ## Gotchas - **claude in the container doesn't see the vault** - `VAULT` in `beaver_agent/vault.py` is `/vault` *inside* the container, not on the host. Don't change it; `BEAVER_VAULT` is for assembling the config on the mac only. - **a claude turn fails immediately** - check `docker logs beaver-gateway` for the `claude[]:` stderr lines. Usually auth: redo step 3. - **gateway restarts in a loop right after first `up`** - `beaver_agent/prompts.py` reads prompt granules from `/vault/мета/бобер/промпты`; until Obsidian Sync has pulled the vault they are missing. Finish step 2, it settles. - **the model cannot write into `мета/бобер`** - the entrypoint grants `beaver-runner` an ACL on the two rw sub-mounts once at start; files that Sync creates later inherit it through the default ACL. If `setfacl` is unsupported on the volume it falls back to `chown`, and then files Sync writes afterwards as root stay read-only for the model until the next restart.