217 lines
9.8 KiB
Markdown
217 lines
9.8 KiB
Markdown
# 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 `<tag>` 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://<DOMAIN>/` 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 <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.
|
|
|
|
```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://<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:
|
|
|
|
```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://<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
|
|
(`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[<agent>]:` 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.
|