2026-05-21 23:54:13 +02:00
2026-05-21 23:54:13 +02:00
2026-05-21 23:54:13 +02:00

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).

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

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

docker exec -it beaver-obsidian ob login
docker exec -it beaver-obsidian ob sync-setup --vault "yourvault"
docker restart beaver-obsidian

Check:

docker exec beaver-obsidian ls /vault

Only markdown is synced by default. To sync everything:

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 <tag> wrappers and the order live in config.py; skills are the folders under мета/бобер/скиллы/ (each becomes a plugin, keep name: in SKILL.md latin). 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 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://<DOMAIN>/ if Caddy), sign in with ADMIN_USER / ADMIN_PASS, go to Tokens → Create, scope * for first run.

5. Smoke test

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 <YOUR_TOKEN>"

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://<DOMAIN>/anthropic, model = agent name (beaver-opus-high etc)
  • MCP clients (Claude Desktop, Raycast extension): /mcp/<name>/, 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://<DOMAIN>/, 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.

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://<DOMAIN> 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:

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:

curl -X POST https://<DOMAIN>/hooks/deploy -H "Authorization: Bearer <TOKEN>"

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 (mcps/komodo.py, архитектура §4.4): 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.

Policy (policy.py)

bypassPermissions everywhere; the boundary is the read-only vault mount plus PreToolUse rules declared per agent (ClaudeAgent.policy, архитектура §3.7): writes only under мета/бобер/, new files only under 💬 чаты/, rm/mv/cp/tee/sed -i/redirects into the vault outside those zones are refused with a reason, and mcp__firefly__store_*/update_* need the firefly skill opened first in the same session. Every tool call lands in the admin Audit page as tool_call. Run make check - it includes the policy and komodo tests.

Editing config.py on the mac

make sync installs ../beaver-gateway editable (extra local), so imports in config.py 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.

Useful commands

# 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 without a full rebuild
docker compose restart gateway

Gotchas

  • claude in the container doesn't see the vault - cwd=VAULT in config.py resolves to /vault inside the container, not on the host. Don't change it.
  • a claude turn fails immediately - check docker logs beaver-gateway for the claude[<agent>]: stderr lines. Usually auth: redo step 3.
  • gateway restarts in a loop right after first up - config.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.
S
Description
My setup for Beaver Agent
Readme
497 KiB
Languages
Python 99.3%
Dockerfile 0.4%
Makefile 0.3%