feat(irc): irc.profullstack.com host, OS-user (tilde.town) gate, premium channels

Hostname: serve the network as irc.profullstack.com (new IRC_DOMAIN var,
default irc.<root-of-DOMAIN>). Caddy serves an irc.profullstack.com site so it
gets a Let's Encrypt cert; ergo-refresh-certs copies that into Ergo for 6697.
Needs an A record irc.profullstack.com -> the box (self-signed until it resolves).

Members are OS users (tilde.town model): setup.sh reconciles a real OS account
per member dir (root-side, on each deploy + the 15-min timer; nologin shell, so
identity-only — no shell access). The IRC auth-script now gates on
`getent passwd` with uid>=1000 instead of the member dir, so "OS user" == member.

Premium channels: free members may /join; creating channels is a premium perk.
The ssh irc@ client gains /create #name (premium-gated via ensurePremium): it
joins the fresh channel and registers it with ChanServ as the member's founder.
v1 gate is route-level (operator-only-creation left off); external-client
creation hardening is a follow-up. See docs/irc.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 15:23:44 +00:00
parent d4ada98b69
commit 5eb1e96480
5 changed files with 194 additions and 87 deletions

View file

@ -4,21 +4,22 @@
# membership. setup.sh installs this to /usr/local/bin/ergo-auth-member and
# wires it into /etc/ergo/ircd.yaml (accounts.auth-script).
#
# "Member" == a user with a home dir under the AgentBBS users dir (created when
# someone registers via `ssh join@`). IRC is members-only, so a login is
# approved iff the requested account name maps to such a dir. The passphrase is
# intentionally IGNORED — membership (a filesystem dir) IS the credential, by
# design (see docs/irc.md). Anyone who knows a member's name can connect as
# them; that tradeoff was chosen deliberately for this private, TLS-only network.
# AgentBBS members are real OS users (tilde.town model; setup.sh provisions an
# OS account per member). IRC is members-only, so a login is approved iff the
# requested account name is a real OS user with uid >= MIN_UID (which excludes
# system accounts like root/ergo/agentbbs). The passphrase is intentionally
# IGNORED — membership (being an OS user) IS the credential, by design (see
# docs/irc.md). Anyone who knows a member's name can connect as them; that
# tradeoff was chosen deliberately for this private, TLS-only network.
#
# Protocol (Ergo): one JSON object on stdin per attempt, one JSON line on stdout
# then exit. Input keys: accountName, passphrase, certfp, ip. Output:
# {"success":bool,"accountName":str,"error":str}.
#
# args: ["<users-dir>"] # defaults to /var/lib/agentbbs/users
# args: ["<min-uid>"] # defaults to 1000
set -uo pipefail
USERS_DIR="${1:-/var/lib/agentbbs/users}"
MIN_UID="${1:-1000}"
# Always emit valid JSON and exit 0 — Ergo reads the JSON, not the exit code;
# a non-zero exit / no output is treated as a script error, not a clean deny.
@ -35,14 +36,22 @@ acct="$(printf '%s' "$line" | jq -r '.accountName // ""' 2>/dev/null || true)"
# certfp-only attempts carry no account name; we don't support cert auth here.
[ -n "$acct" ] || deny "membership requires an account name"
# Defense in depth against path traversal. IRC account names are a restricted
# charset anyway, but never let one escape USERS_DIR.
# Restrict to plain login names (defense in depth; IRC names are limited anyway).
case "$acct" in
*[!A-Za-z0-9._-]* | "." | ".." | *..* | */* ) deny "invalid account name" ;;
*[!A-Za-z0-9._-]* | "." | ".." ) deny "invalid account name" ;;
esac
if [ -d "$USERS_DIR/$acct" ]; then
# Resolve the OS account; getent passwd returns name:passwd:uid:gid:...
entry="$(getent passwd "$acct" 2>/dev/null || true)"
[ -n "$entry" ] || deny "not a member"
uid="$(printf '%s' "$entry" | cut -d: -f3)"
case "$uid" in
''|*[!0-9]*) deny "not a member" ;;
esac
if [ "$uid" -ge "$MIN_UID" ]; then
printf '{"success":true,"accountName":"%s"}\n' "$acct"
else
deny "not a member"
deny "system accounts cannot use IRC"
fi

View file

@ -6,11 +6,11 @@
#
# __NETWORK__ network name shown to clients (e.g. ProfullstackBBS)
# __DOMAIN__ the BBS domain (e.g. bbs.profullstack.com)
# __IRC_DOMAIN__ the IRC server name + TLS cert host (e.g. irc.profullstack.com)
# __DATA__ Ergo state dir (ircd.db lives here)
# __TLS_DIR__ dir holding fullchain.pem / privkey.pem for 6697
# __LANG_DIR__ Ergo's bundled languages/ dir (from the release)
# __OPER_PASSWORD_HASH__ bcrypt hash for /OPER admin (ergo genpasswd)
# __USERS_DIR__ AgentBBS member home dirs (the IRC membership gate)
#
# It is based on Ergo's upstream default.yaml (v2.18.0) with the AgentBBS
# listeners (public 6697 TLS, loopback 6667, loopback 8097 WebSocket fronted
@ -18,15 +18,16 @@
# for a mixed humans + agents network: it is MEMBERS-ONLY — every client must
# authenticate with SASL, self-service registration is OFF, and an auth-script
# (deploy/ergo/auth-script.sh, installed as /usr/local/bin/ergo-auth-member)
# approves a login only if the account name maps to an existing AgentBBS member
# home dir under __USERS_DIR__. Message history (CHATHISTORY) is enabled so
# approves a login only if the account name is a real OS user (uid>=1000).
# BBS members are provisioned as OS users (tilde.town model; setup.sh §4b/§9a2),
# so "OS user" == "BBS member". Message history (CHATHISTORY) is enabled so
# reconnecting agents and web clients can replay. See docs/irc.md.
#
# Most settings keep Ergo's recommended defaults — read the inline comments
# before changing one. A few worth knowing about:
# 1. network.name / server.name — the network identity (rendered from tokens).
# 2. server.listeners — the 6697 cert/key are refreshed from Caddy's Let's
# Encrypt cert for __DOMAIN__ by setup.sh (self-signed fallback on first run).
# Encrypt cert for __IRC_DOMAIN__ by setup.sh (self-signed fallback on first run).
# 3. opers — the /OPER admin password hash (rendered from a token).
# 4. history — in-memory, messages expire after ~7 days. Switch to MySQL-backed
# persistent history (datastore.mysql) if you need durability across restarts.
@ -39,13 +40,13 @@ network:
# server configuration
server:
# server name
name: irc.__DOMAIN__
name: __IRC_DOMAIN__
# addresses to listen on
listeners:
# Public TLS (6697) — the front door for native IRC clients (humans) and
# SASL-authenticating agents. Cert/key are refreshed from Caddy's
# Let's Encrypt cert for __DOMAIN__ by setup.sh (falls back to self-signed).
# Let's Encrypt cert for __IRC_DOMAIN__ by setup.sh (falls back to self-signed).
":6697":
tls:
cert: __TLS_DIR__/fullchain.pem
@ -597,12 +598,12 @@ accounts:
# see the manual for details on how to write an authentication plugin script
auth-script:
# MEMBERS-ONLY gate: ergo-auth-member approves a login iff the account
# name maps to an existing AgentBBS member home dir (passed as the arg).
# name is a real OS user with uid >= the arg (BBS members are OS users).
enabled: true
command: "/usr/local/bin/ergo-auth-member"
# the AgentBBS users dir is passed as a constant arg; the per-attempt
# auth data (accountName/passphrase/ip) is sent over stdin/stdout:
args: ["__USERS_DIR__"]
# min-uid is passed as a constant arg (excludes system accounts like
# ergo/root); the per-attempt auth data is sent over stdin/stdout:
args: ["1000"]
# auto-create the Ergo account on first successful (member) auth, so
# members never have to register:
autocreate: true