Skip to content

patryksg/fatbot

Repository files navigation

fatbot

Fork of Limnoria — the original bot framework by Valentin Lorentz et al. This repo contains the upstream Limnoria core plus custom plugins specific to fatbot.

→ Setup guide: Debian Trixie from scratch

⚠️ VIBECODED — USE AT YOUR OWN RISK

This entire codebase was written with AI assistance (Claude Code / Claude Sonnet). It has not been formally audited. It runs a bot connected to a public IRC network, exposes a subprocess-based LLM interface to channel users, and handles untrusted URLs from the internet. Known risks include prompt injection, SSRF, session-token leakage, and general "AI wrote this" bugs. Read the code before deploying it. Security mitigations for the Claude plugin are documented in the Claude plugin security section below.

Layout

  • Bot home: /home/botuser/runbot/
  • Master config: fatbot.conf (rewritten on shutdown — see "conf-rewrite gotcha")
  • Per-section conf: conf/{channels,users,networks}.conf (also rewritten on shutdown)
  • Custom plugins: plugins/<Name>/
  • Channel logs: logs/ChannelLogger/<network>/<#channel>/
  • Cookie jars (mode 600): reddit-cookies.txt, youtube-cookies.txt (see "Session cookies" below)
  • Channel digest: chaninfo.md (nightly summary, generated by the Brain plugin)

Dependencies

Python packages (injected into the pipx venv)

pipx inject limnoria curl_cffi
pipx inject limnoria PySocks --pip-args="--no-deps"
  • curl_cffi — used by the Title plugin's HTTP layer to impersonate chrome131 and bypass TLS-fingerprint bot detection (Akamai, Cloudflare). Single shared cc.Session(), cookie jar persistence, SSRF guard via getaddrinfo.
  • PySocks — required for socks5h:// URLs in requests/urllib, used to route a retry through cloudflare-warp when a page returns a bot challenge (see the Title plugin's WARP two-pass below).

System: cloudflare-warp (SOCKS5 proxy mode)

Debian package cloudflare-warp (repo https://pkg.cloudflareclient.com bookworm main, signed-by /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg).

Runs in SOCKS5 proxy mode on 127.0.0.1:40000, NOT as a default route. A default-route ("warp" mode) install will kill SSH because warp injects a tunnel interface and reroutes all egress.

Why: some sites front their pages with a Cloudflare/Akamai anti-bot challenge that blocks VPS egress IPs. The Title plugin retries such a fetch through WARP (a residential-looking egress) to get the real page; everything else continues to use the normal VPS egress. (Historically this proxy was used by the ShrinkUrl plugin for is.gd, which has since been removed — WARP is now optional and only used by the Title two-pass.)

Setup / recovery sequence:

apt install cloudflare-warp
warp-cli --accept-tos registration new
warp-cli --accept-tos mode proxy           # MUST be set before connect
warp-cli --accept-tos proxy port 40000
warp-cli --accept-tos connect

Verify:

  • warp-cli --accept-tos statusConnected
  • warp-cli --accept-tos settingsMode: WarpProxy on port 40000
  • ip route default still points at your gateway; no CloudflareWARP interface in ip link
  • ss -tlnp | grep 40000 shows the local SOCKS5 listener
  • curl -x socks5h://127.0.0.1:40000 https://www.cloudflare.com/cdn-cgi/trace returns warp=on; direct curl returns warp=off

Persistence: warp-svc.service is enabled; Always On: true in settings; reg.json + settings.json in /var/lib/cloudflare-warp/ persist registration and mode across reboots.

Consumer: the Title plugin's bot-challenge two-pass retry. If WARP is disconnected, the fetch simply stays on direct egress — the bot stays functional, it just can't retry challenge-walled pages.

systemd

  • Unit: /etc/systemd/system/fatbot.service
  • Drop-ins: /etc/systemd/system/fatbot.service.d/{hardening,claude}.conf
  • Control: sudo systemctl {start,stop,restart,status} fatbot
  • Logs: sudo journalctl -u fatbot -f and /home/botuser/runbot/logs/{messages,error}.log

Sandbox

ProtectHome=read-only with ReadWritePaths=/home/botuser/runbot, PrivateTmp=true. Subprocesses allowed (yt-dlp, claude). Hardening: caps dropped, seccomp @system-service ~@privileged, MDWE, PrivateDevices, ProtectProc=invisible. Score ~1.5 via systemd-analyze security.

Seccomp gotcha: do NOT add ~@resources to SystemCallFilter — the bundled claude binary (V8 JIT) needs scheduling/rlimit syscalls and SIGSYS-crashes on startup if they're blocked.

XDG redirect for subprocess plugins writing to $HOME: pass XDG_CACHE_HOME=/home/botuser/runbot/.cache, XDG_CONFIG_HOME=/home/botuser/runbot/.config, CLAUDE_CONFIG_DIR=/home/botuser/runbot/.claude so writes stay inside ReadWritePaths.

conf-rewrite gotcha

supybot rewrites fatbot.conf and conf/*.conf on shutdown with its in-memory state. To edit any of those:

  1. systemctl stop fatbot
  2. edit
  3. systemctl start fatbot

A restart after editing while the bot is running silently reverts the edits. Plugin code (plugins/<Name>/plugin.py) is safe to edit live — those aren't rewritten:

rm -rf plugins/<Name>/__pycache__
systemctl restart fatbot     # or `!reload <Name>` / `!rl` (Reload plugin) from IRC

Plugins

Custom plugins live in plugins/. Each plugin must be added to fatbot.conf:

  • supybot.plugins: ... <Name> (space-separated)
  • supybot.plugins.<Name>: True

ChannelLogger (stock) is also loaded but does not appear in the supybot.plugins: line.

Custom plugins currently installed:

  • Claude — Claude-CLI-backed chat / Q&A, asked by addressing the bot by nick; !claude/!haiku (default, cheap) and !fable (top model, --effort max, expensive) switch the per-channel mode. Per-channel "brain" recall. The Gemini chat fallback was removed 2026-06-13 — Gemini is only used by the MCP video tools. Exposes a small MCP tool suite to the model: an image viewer, a URL fetcher, YouTube transcript/download tools, and a Reddit-video analyzer (Gemini Files API, with optional rehosting on the Zipline image host). See Claude plugin security
  • Create — image / video generation (!pic / !picnsfw / !video / !videonsfw) via Gemini and Runware, rehosted on a Zipline image host; channel-gated by a generative capability
  • EasyControl — consolidated user/channel admin commands (cap/chancap/adduser/chanmode/topic …) plus channel-mode and topic enforcement. Supersedes the older ChanModes, InfoToggle, Aliases and TopicLock plugins, which remain in the tree but are no longer loaded.
  • Seen!seen <nick> last-activity lookup, gated by a per-channel seen capability
  • Title — URL-title snarfer using curl_cffi; combined-mode with ShrinkUrl posts <title> | <short>; per-site brand prefixes and reddit/twitter special-casing (rewrites to old.reddit.com, uses api.fxtwitter.com); WARP two-pass retry on bot-challenge pages
  • ShrinkUrl — overridden to default to t.ly (Bearer-token API), falling back through tinyurl → x0.no
  • YouTube — yt-dlp-backed metadata snarfer, plus !ytdl <url> which downloads a video and rehosts it on the Zipline image host (gated by a per-channel ytdl capability)
  • SportsNews!sports <league> posts the top 3 ESPN headlines for a league (17 supported: nfl, nba, mlb, nhl, epl, laliga, ucl, ufc, f1, …), article URLs shortened via ShrinkUrl
  • Reload!rl batch-reloads every plugin listed in supybot.plugins.Reload.plugins in one shot (owner-only); stock reload only takes one plugin at a time
  • Ash — Evil-Dead "Ash Williams" quote / persona responder
  • Wikibear — creepy-cheerful Wikipedia fact responder
  • Relay — relays public messages between two channels
  • Greeter, Hamster, Repo — small utility plugins
  • ChanModes, InfoToggle, Aliases, TopicLock — legacy plugins, folded into EasyControl and no longer loaded

Session cookies (reddit, YouTube)

Two plugins read Netscape-format cookies.txt files containing live session cookies from a logged-in browser. These are required because both sites now block anonymous requests from VPS egress IPs; TLS-fingerprint impersonation alone is not enough.

File Consumer Config key Why
reddit-cookies.txt Title plugin (HTTP fetch path) supybot.plugins.Title.cookiesFile Reddit blocks unauthenticated requests from VPS IPs. With a reddit_session cookie, old.reddit.com returns a normal page (the new app's title only appears at ~430 KB offset, past our 256 KB cap — so the plugin rewrites (www.)?reddit.comold.reddit.com before fetching).
youtube-cookies.txt YouTube plugin (yt-dlp subprocess) supybot.plugins.YouTube.cookiesFile yt-dlp otherwise hits "Sign in to confirm you're not a bot" on metadata extraction. A logged-in cookie jar bypasses that gate.

How to provide them:

  1. In a logged-in browser session, export cookies.txt for the target domain (browser extension or yt-dlp --cookies-from-browser).
  2. Save with mode 600, owned by the bot user.
  3. Point the config keys at the absolute path.

The Title plugin auto-reloads its jar when the file's mtime changes, so refreshing cookies doesn't need a bot restart. Cookies expire — re-export periodically.

Security note: these files contain live session tokens. Never commit them. The bot's .gitignore excludes *-cookies.txt.

Claude plugin security

The Claude plugin spawns claude (Claude Code CLI) as a subprocess and relays its output to an IRC channel. Access is restricted: the plugin must be enabled for the channel, and the user must be a registered Limnoria user with the claude channel capability explicitly granted. The attack surface is therefore limited to trusted, named users — not the general public. The following mitigations are in place regardless.

Hard stops (cannot be bypassed by the model or a prompt)

Mitigation How
No tool access --tools "" passed on every invocation — claude cannot shell out, read files, or make network requests regardless of what a prompt tells it to do.
No shell Plugin builds argv as a Python list and passes the question via stdin. No shell=True, no interpolation, no command-injection surface.
Email redaction EMAIL_RE strips any email-shaped string from every output line before it reaches the channel. Claude Code injects the OAuth account email into model context regardless of --system-prompt; the regex catches leakage even across future model updates.
Rate/token line suppression LEAK_LINE_RE drops any output line mentioning rate limits, token usage, or pricing.
Stderr never relayed subprocess stderr is captured and discarded; internal errors don't reach the channel.
Capability gate Both the !claude command and the nick-addressed doPrivmsg listener check ircdb.checkCapability for a per-channel claude cap. Only explicitly registered users hold this cap — channel-default was deliberately not used to avoid granting access to unregistered users.
Public-only The !claude command uses Limnoria's 'public' wrap filter; doPrivmsg filters by channel prefix. PMs are blocked.

Soft stops (model-level, not cryptographically guaranteed)

Mitigation How
System prompt Instructs the model to reply concisely, avoid disclaimers, and not relay internal context. Soft barrier — overridable by a sufficiently clever prompt, which is why the hard stops above exist.
No session persistence --no-session-persistence --disable-slash-commands passed on every call. Context is synthetic (Q/A pairs prepended to stdin), lost on restart.
Per-user context TTL In-memory context store with a 6-minute sliding TTL, max 5 turns. Not persisted to disk.

Systemd sandbox

The fatbot service itself runs under ProtectHome=read-only with ReadWritePaths=/home/botuser/runbot, PrivateTmp=true, dropped capabilities, seccomp filter (@system-service ~@privileged), MDWE, PrivateDevices, ProtectProc=invisible. The claude subprocess inherits this sandbox. XDG env vars redirect any writes into ReadWritePaths so nothing escapes to $HOME.

Known remaining risks:

  • --tools "" is a CLI flag, not a kernel guarantee. A future Claude Code update could change flag semantics.
  • DNS rebinding: the Title/image SSRF guard re-validates hosts via getaddrinfo but curl_cffi 0.15 doesn't support resolve=, so curl re-resolves at connect time (TOCTOU window).
  • Session cookies (reddit-cookies.txt, youtube-cookies.txt) contain live browser tokens — if the bot process is compromised they're exposed.
  • The MCP tools (mcp_imageview.py, mcp_fetch.py, mcp_youtube.py, mcp_reddit.py) fetch arbitrary URLs from IRC on model request — image viewing, generic fetch, and YouTube/Reddit video download + Gemini Files API analysis with optional Zipline rehosting. They are SSRF-guarded and scope-limited (image-only, YouTube/Reddit hosts), but the network attack surface exists and the video tools shell out to yt-dlp/ffmpeg.

Command prefix

! — configured via supybot.reply.whenAddressedBy.chars. supybot.reply.whenNotCommand: False is set deliberately so the Claude plugin's nick-addressed doPrivmsg doesn't produce "not a valid command" noise.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages