748048be50
Sessions run claude with --dangerously-skip-permissions and a prompt built from issue/PR/comment text, so only trusted authors may supply that text. teabot now dispatches a job only when the triggering event's author login is on an allowlist; an empty allowlist dispatches nothing (fail-closed). Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
88 lines
4.4 KiB
Markdown
88 lines
4.4 KiB
Markdown
# Architecture
|
|
|
|
teabot is a single binary with a small set of internal packages. The daemon
|
|
polls Gitea, decides which events warrant work, and runs each unit of work as a
|
|
disposable Docker container.
|
|
|
|
```
|
|
main.go
|
|
└── internal/cli cobra command tree (run, config)
|
|
└── internal/dispatch poll loop, event filtering, job orchestration
|
|
├── internal/gitea read-only Gitea REST client
|
|
├── internal/state processed-event persistence (JSON)
|
|
├── internal/prompt per-event prompt construction
|
|
├── internal/config config + tea-config parsing
|
|
└── internal/docker containerised Claude execution (Runner iface)
|
|
```
|
|
|
|
## Poll cycle
|
|
|
|
`dispatch.Dispatcher` runs one `pollRepo` per watched repo each interval:
|
|
|
|
1. **List** open issues, open pull requests, and recent comments via the Gitea
|
|
API (one client authenticated as the first personality — reads only).
|
|
2. **Seed on first contact.** The very first poll of a repo records everything
|
|
currently open/recent as *processed* without dispatching anything, so a fresh
|
|
install does not stampede every existing item. The `seeded` flag is persisted
|
|
per repo.
|
|
3. **Classify & dispatch** subsequent events:
|
|
- a new issue → implementer session,
|
|
- a new pull request → reviewer session,
|
|
- a new comment on a thread teabot **acted on** → follow-up session.
|
|
|
|
## Filtering rules (loop prevention + dedup)
|
|
|
|
Three independent guards decide whether an event becomes a job:
|
|
|
|
- **Loop prevention** — any event authored by one of teabot's own personality
|
|
logins is skipped. This is what stops the bot reacting to its own PRs and
|
|
comments in an infinite loop. Bot-authored items are still *recorded* as
|
|
processed so they are never reconsidered.
|
|
- **Author allowlist (security)** — because jobs run
|
|
`--dangerously-skip-permissions` with a prompt built from event text, teabot
|
|
dispatches only for events whose author is on the `allowed_authors` list
|
|
(with an optional per-repo override). This is **fail-closed**: an empty
|
|
allowlist dispatches nothing. Non-allowlisted events are recorded (never
|
|
re-triggered) and logged, but never spawn a container. Comment follow-ups are
|
|
gated on the *new comment's* author, so an untrusted comment on a bot thread
|
|
cannot reopen the injection path. See
|
|
[configuration](configuration.md#author-allowlist-security).
|
|
- **Dedup** — every dispatched issue/PR index and every seen comment ID is
|
|
recorded in the state store (`~/.local/state/teabot/state.json`). An item is
|
|
marked processed *before* its job starts, so a subsequent poll (or a restart
|
|
mid-job) cannot double-launch it.
|
|
|
|
Comment follow-ups additionally require the parent issue/PR to be in the
|
|
*acted-on* set — teabot only continues threads it started, never arbitrary
|
|
comment threads. See [configuration](configuration.md) for the implication.
|
|
|
|
## Job execution
|
|
|
|
Each session is a `docker.Job` handed to a `docker.Runner`. The production
|
|
`DockerRunner`:
|
|
|
|
1. Materialises a per-job scratch dir containing the prompt, a static
|
|
`job.sh` entrypoint, a **private copy** of the Claude config dir (so
|
|
subscription-token refreshes never mutate the host's `~/.claude`), and a copy
|
|
of the personality's tea config.
|
|
2. Runs `docker run --rm --entrypoint /bin/bash <image> /teabot/job.sh` with:
|
|
- the scratch files bind-mounted (`:ro,z` under SELinux),
|
|
- the tea config mounted at `$HOME/.config/tea/config.yml` and
|
|
`XDG_CONFIG_HOME` set, so `tea` acts as the bot identity,
|
|
- git identity + a credential-store token so clones and pushes authenticate,
|
|
- `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL` injected only when configured
|
|
(otherwise the mounted subscription credentials are used).
|
|
3. `job.sh` configures git, clones the repo, and runs
|
|
`claude --print --dangerously-skip-permissions < /teabot/prompt.txt`.
|
|
|
|
The `Runner` interface means the whole dispatch layer is unit-testable with a
|
|
fake runner — no Docker daemon required. Concurrency is bounded by a semaphore
|
|
sized to `max_concurrent`, and every job has a `job_timeout`.
|
|
|
|
## Container image
|
|
|
|
teabot reuses the existing **`git.unkin.net/unkin/agent-dev:latest`** image,
|
|
which already bundles the `claude` CLI plus a Go/Node/Python/`tea` developer
|
|
toolchain and language servers. The image is configurable via `job_image`, so a
|
|
purpose-built image can be substituted without code changes.
|