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
4.4 KiB
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:
- List open issues, open pull requests, and recent comments via the Gitea API (one client authenticated as the first personality — reads only).
- 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
seededflag is persisted per repo. - 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-permissionswith a prompt built from event text, teabot dispatches only for events whose author is on theallowed_authorslist (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. - 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 for the implication.
Job execution
Each session is a docker.Job handed to a docker.Runner. The production
DockerRunner:
- Materialises a per-job scratch dir containing the prompt, a static
job.shentrypoint, 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. - Runs
docker run --rm --entrypoint /bin/bash <image> /teabot/job.shwith:- the scratch files bind-mounted (
:ro,zunder SELinux), - the tea config mounted at
$HOME/.config/tea/config.ymlandXDG_CONFIG_HOMEset, soteaacts as the bot identity, - git identity + a credential-store token so clones and pushes authenticate,
ANTHROPIC_API_KEY/ANTHROPIC_BASE_URLinjected only when configured (otherwise the mounted subscription credentials are used).
- the scratch files bind-mounted (
job.shconfigures git, clones the repo, and runsclaude --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.