# beaver-agent My real beaver-gateway setup (paired with the protocol from beaver.kotikot.com). 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. ## 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 - **Admin UI:** `http://localhost:62990/` or `https:///`, login from `ADMIN_USER` / `ADMIN_PASS` ## 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` - `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.