Add fail-closed author allowlist gating job dispatch
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

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
This commit is contained in:
2026-07-27 00:33:09 +10:00
parent 3be3f4cc46
commit 748048be50
14 changed files with 413 additions and 6 deletions
+10 -1
View File
@@ -32,12 +32,21 @@ main.go
## Filtering rules (loop prevention + dedup)
Two independent guards decide whether an event becomes a job:
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
+41
View File
@@ -21,6 +21,8 @@ Override the path with `--config/-c`. Generate a starting point with
| `anthropic_api_key` | *(unset)* | If set, injected as `ANTHROPIC_API_KEY`. |
| `anthropic_base_url` | *(unset)* | If set, injected as `ANTHROPIC_BASE_URL`. |
| `state_dir` | `~/.local/state/teabot` | Where processed-event state is persisted. |
| `allowed_authors` | *(empty → fail-closed)* | Gitea usernames whose events may trigger a job. See [Author allowlist](#author-allowlist-security). |
| `repo_allowed_authors` | *(unset)* | Optional per-repo override of `allowed_authors`, keyed by `owner/name`. |
| `personalities` | *(required)* | Bot identities (see below). |
`~` and `~/` are expanded in path-valued keys.
@@ -73,6 +75,45 @@ prevention** (it never reacts to events authored by any personality's username).
At least one personality must be able to implement and at least one to review,
or config validation fails.
## Author allowlist (security)
Each job runs `claude --print --dangerously-skip-permissions` inside a container,
and its **prompt is built from event text** — the issue title/body, PR
description/diff, and comments. That text is attacker-controllable: anyone who
can open an issue or comment on the repo could otherwise inject instructions
into a session that has permissions disabled. The allowlist is the control that
closes this: teabot dispatches a job **only** when the triggering event's author
login is on the allowlist.
```yaml
# Global allowlist.
allowed_authors:
- benvin
- trusted-colleague
# Optional per-repo override. A present entry fully REPLACES the global list for
# that repo; an empty list disables dispatch for it; an absent entry falls back
# to the global list.
repo_allowed_authors:
unkin/teabot:
- benvin
```
Semantics:
- **Fail-closed.** An empty/absent allowlist dispatches **nothing**. teabot logs
a warning at startup when no allowlist is configured anywhere.
- Events from non-allowlisted authors are **recorded** as processed (so they
don't re-trigger) and logged at info with the author name, but never spawn a
container. They are *not* marked as engaged threads.
- **Comment follow-ups are gated on the new comment's author**, not just the
thread. A comment from a non-allowlisted user on a thread teabot previously
acted on is ignored — otherwise an untrusted comment could reopen the
injection path on a bot thread.
- Matching is case-insensitive (Gitea usernames are unique case-insensitively).
- This is **separate from and in addition to** loop prevention: bot personality
logins are always excluded first, even if one were listed here.
## Claude credentials
By default teabot uses your Claude **subscription** auth: it copies