Watch a repo for commits pushed by anyone but me
The bot only ever saw pull requests, so a collaborator with write access who pushes straight to master was invisible: 18 of the last 50 commits on ten31-brand's master arrived that way, unreviewed and unannounced. Opt-in per repo (watch_pushes + [gitea] my_emails). Each poll cycle it pages commits newer than a stored cursor across every branch except ones with an open PR, keeps the commits whose git email isn't mine, posts a notice naming author, branch, commits and files, records them, and only then runs a claude -p assessment threaded under it. Identity comes from the git email because these commits aren't linked to Gitea accounts. The notice is deterministic Gitea data and the "outside the expected area" flag is computed from globs, so the thing you actually need never depends on a model call. Recording before the assessment is what stops a crash in those minutes from reposting the whole notice. Both prompts now put every attacker-authored field — titles, commit subjects, filenames, the diff — inside a nonce-tagged fence, so a forged terminator can't reach the instruction region. Folds in the ROADMAP item to bound two unbounded awaits and exit if the poll task dies, and adds the repo's first test: the watch policy against a real-shaped Gitea fixture, with 17 mutations confirmed to fail it.
This commit is contained in:
+14
-6
@@ -17,12 +17,20 @@ Phases beyond the first milestone (threaded single-flow review + merge/reject/de
|
||||
PRs in both rooms at the same time.
|
||||
|
||||
## Fold-ins — do opportunistically, never a session of their own
|
||||
- *(fold-in, not a session of its own)* Next time `src/bot.py`'s SSH or Matrix-send path is edited:
|
||||
bound the two unbounded awaits (`await proc.wait()` after the timeout kill in `ssh_run`;
|
||||
`client.room_send` in `say()`), and attach a done-callback to the poll task that stops the process
|
||||
if it ever dies, so the existing `restart: unless-stopped` + spark-control tile cover it. No hang
|
||||
has been observed; this is "unbounded is not a defensible default in a loop with no supervisor",
|
||||
not a fix for a diagnosed bug. *(adjudicated 2026-07-29: fold-in only)*
|
||||
- ~~Bound the two unbounded awaits and die if the poll task dies.~~ **Done 2026-08-21**, folded into
|
||||
the push-watch change: `ssh_run`'s post-kill `proc.wait()` and `say()`'s `client.room_send` are both
|
||||
`wait_for`-bounded, and a poll-task death now `os._exit(1)`s so `restart: unless-stopped` brings the
|
||||
bot back instead of leaving it synced but reviewing nothing.
|
||||
|
||||
## Next up
|
||||
- **Restrict what the assessment/review session may do.** A PR diff and a pushed commit are both
|
||||
written by someone else, and they reach `claude -p` through matrix-bridge's `ask-claude.sh`, which
|
||||
exports the owner's OAuth token and runs in the owner's live checkout with the default tool policy.
|
||||
Both prompts now fence the diff as untrusted data, which is the cheap half; the real fix is an
|
||||
explicit read-only tool policy (`--allowedTools Read,Grep,Glob,Task`) on that run. It needs a
|
||||
**matrix-bridge change** (a `MB_CLAUDE_TOOLS` env passthrough alongside the existing
|
||||
`MB_CLAUDE_MODEL`/`MB_CLAUDE_EFFORT`), so it is a two-repo change and affects the PR review path
|
||||
too. *(raised by the 2026-08-21 reviewer pass on the push watch.)*
|
||||
|
||||
## Deferred / non-goals
|
||||
- Not a Claude-session bot (that's matrix-bridge); not Maubot (revisit at ~6+ bots or web-UI mgmt);
|
||||
|
||||
Reference in New Issue
Block a user