Autonomous deploy + free-pod/Premium-email membership

Deploy automation (idempotent, runs on every deploy):
- .github/workflows/deploy.yml: push to main/master (or dispatch) SSHes to the
  droplet and re-runs setup.sh; deploys the pushed branch; smoke-tests :22.
- scripts/self-update.sh + agentbbs-update.timer: autonomous backstop that
  redeploys only when origin advances.
- setup.sh hardened: flock, fetch+reset (survives force-push), fixed the
  always-skipped arcade asset fetch path.

Membership model:
- Free, email-verified members get their own Docker pod (pod@ paywall removed)
  and a /~name homepage (seeded at join@).
- join@ is now interactive: email -> emailed 6-digit code -> enter code.
- Premium ($10 one-time, lifetime via CoinPay) grants a personal
  <name>@host email (new internal/forwardemail; forwardemail.net aliases) and
  custom domains (domain@ gated to Premium).
- ensurePremium() silently verifies/grants/provisions on hub login, join@, and
  domain@. New-signup details emailed to AGENTBBS_SIGNUP_NOTIFY (subject "bbs").

Store: User.Premium + premium/premium_ref cols, ConfirmEmailCode, GrantPremium.
Tests: store_premium_test.go, forwardemail_test.go. Build/vet/gofmt/test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 07:58:51 +00:00
parent 1086d57a4d
commit 3230807421
11 changed files with 900 additions and 101 deletions

View file

@ -26,6 +26,7 @@ ADMIN_SSH_PORT="${ADMIN_SSH_PORT:-2202}"
ACME_EMAIL="${ACME_EMAIL:-admin@profullstack.com}"
SVC_USER="${SVC_USER:-agentbbs}"
REPO="${REPO:-https://github.com/profullstack/agentbbs.git}"
BRANCH="${BRANCH:-main}"
SRC_DIR="${SRC_DIR:-/opt/agentbbs}"
DATA_DIR="${DATA_DIR:-/var/lib/agentbbs}"
ASK_ADDR="${ASK_ADDR:-127.0.0.1:8081}" # agentbbs on-demand-TLS ask endpoint (must match agentbbs.env)
@ -33,12 +34,23 @@ HTTP_ADDR="${HTTP_ADDR:-127.0.0.1:8088}" # agentbbs /verify endpoint (join@ emai
GO_VERSION="${GO_VERSION:-1.26.4}"
POD_IMAGE="${POD_IMAGE:-docker.io/library/ubuntu:24.04}"
FETCH_ASSETS="${FETCH_ASSETS:-1}" # set 0 to skip the DOOM/Freedoom arcade assets
SELF_UPDATE="${SELF_UPDATE:-1}" # set 0 to skip the autonomous self-update systemd timer
SELF_UPDATE_INTERVAL="${SELF_UPDATE_INTERVAL:-15min}" # how often the box polls origin for new commits
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "run as root (sudo ./setup.sh)"
# Serialize runs. A CI deploy (ssh -> setup.sh) and the self-update timer can
# fire close together; two concurrent git-reset + go-build runs would corrupt
# each other. Hold an exclusive lock for the whole run (wait up to 5 min).
if command -v flock >/dev/null; then
exec 9>/var/lock/agentbbs-setup.lock
flock -w 300 9 || die "another setup.sh run is in progress (lock held >5m)"
fi
. /etc/os-release 2>/dev/null || true
[ "${ID:-}" = "ubuntu" ] || warn "tested on Ubuntu; ${ID:-unknown} may differ"
@ -113,16 +125,19 @@ chown "$SVC_USER:$SVC_USER" "$DATA_DIR/web/index.html"
# ---- 5. clone/update + build agentbbs --------------------------------------
if [ -d "$SRC_DIR/.git" ]; then
log "updating source in $SRC_DIR"
git -C "$SRC_DIR" pull --ff-only
log "updating source in $SRC_DIR to origin/$BRANCH"
# Hard reset (not pull --ff-only) so an automated deploy survives a force-push
# or any local drift on the box — the box always matches origin exactly.
git -C "$SRC_DIR" fetch --depth 1 origin "$BRANCH"
git -C "$SRC_DIR" reset --hard "origin/$BRANCH"
else
log "cloning $REPO"
git clone --depth 1 "$REPO" "$SRC_DIR"
log "cloning $REPO ($BRANCH)"
git clone --depth 1 -b "$BRANCH" "$REPO" "$SRC_DIR"
fi
if [ "$FETCH_ASSETS" = "1" ] && [ -x "$SRC_DIR/fetch-assets.sh" ]; then
if [ "$FETCH_ASSETS" = "1" ] && [ -x "$SRC_DIR/scripts/fetch-assets.sh" ]; then
log "fetching arcade assets (set FETCH_ASSETS=0 to skip)"
( cd "$SRC_DIR" && ./fetch-assets.sh ) || warn "asset fetch failed; arcade may be limited"
( cd "$SRC_DIR" && ./scripts/fetch-assets.sh ) || warn "asset fetch failed; arcade may be limited"
fi
log "building binaries"
@ -159,12 +174,33 @@ AGENTBBS_HTTP_ADDR=${HTTP_ADDR}
# AGENTBBS_SMTP_USER=
# AGENTBBS_SMTP_PASS=
# AGENTBBS_SMTP_FROM=bbs@${DOMAIN}
# pod@ requires a verified email; set 0 to disable on a dev host:
# Free pods + custom homepages require a verified email; set 0 to disable on a
# dev host (then any registered key gets a pod):
# AGENTBBS_REQUIRE_VERIFIED_EMAIL=1
# Every new signup is emailed here (subject "bbs"); needs SMTP configured above:
# AGENTBBS_SIGNUP_NOTIFY=anthony@profullstack.com
# Pods (CoinPay \$1/mo membership) — required for pod@ to charge/verify:
# AGENTBBS_COINPAY_PAY_TMPL=
# AGENTBBS_COINPAY_VERIFY_CMD=
# Membership model:
# Free verified members get their own Docker pod (ssh pod@) and a homepage
# at https://${DOMAIN}/~<name>.
# Premium \$10 one-time, lifetime — a personal <name>@${DOMAIN} email
# (forwardemail.net) plus custom domains (ssh domain@). Offered at join@.
# Premium payment via the coinpay CLI: join@ mints a charge and shows the amount
# + deposit address; the status command verifies a later settlement. %s is the
# per-account payment reference.
# AGENTBBS_PREMIUM_AMOUNT=10
# AGENTBBS_PREMIUM_CURRENCY=USD
# AGENTBBS_PREMIUM_BLOCKCHAIN=eth
# AGENTBBS_COINPAY_PREMIUM_CREATE_CMD=coinpay payment create --amount 10 --currency USD --blockchain eth --json --metadata %s
# AGENTBBS_COINPAY_PREMIUM_PAY_TMPL=coinpay payment create --amount 10 --currency USD --blockchain eth --metadata %s
# AGENTBBS_COINPAY_PREMIUM_STATUS_CMD=coinpay payment status %s
# Premium email aliases (<name>@${DOMAIN}) auto-created on forwardemail.net.
# Without an API key the address is shown but not created (add it manually).
# AGENTBBS_FORWARDEMAIL_API_KEY=
# AGENTBBS_FORWARDEMAIL_DOMAIN=${DOMAIN}
# AGENTBBS_WEBMAIL_URL=https://webmail.${DOMAIN}
# PairUX video calls rendered as ASCII (video@ / tv@ PairUX sources):
# AGENTBBS_LIVEKIT_URL=
@ -207,6 +243,46 @@ WantedBy=multi-user.target
UNIT
systemctl daemon-reload
# ---- 7b. autonomous self-update timer (poll origin, redeploy on new commits) -
if [ "$SELF_UPDATE" = "1" ]; then
log "installing self-update timer (every ${SELF_UPDATE_INTERVAL}; set SELF_UPDATE=0 to disable)"
cat > /etc/systemd/system/agentbbs-update.service <<UNIT
[Unit]
Description=AgentBBS self-update (pull origin/${BRANCH} + redeploy if changed)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
Environment=REPO=${REPO}
Environment=BRANCH=${BRANCH}
Environment=SRC_DIR=${SRC_DIR}
# Pass through the same overrides this provisioner ran with so a timer-driven
# redeploy is identical to this one.
Environment=DOMAIN=${DOMAIN}
Environment=ADMIN_SSH_PORT=${ADMIN_SSH_PORT}
ExecStart=${SRC_DIR}/scripts/self-update.sh
UNIT
cat > /etc/systemd/system/agentbbs-update.timer <<UNIT
[Unit]
Description=Poll for AgentBBS updates and redeploy
[Timer]
OnBootSec=3min
OnUnitActiveSec=${SELF_UPDATE_INTERVAL}
Persistent=true
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload
systemctl enable --now agentbbs-update.timer >/dev/null 2>&1 || true
else
systemctl disable --now agentbbs-update.timer >/dev/null 2>&1 || true
rm -f /etc/systemd/system/agentbbs-update.service /etc/systemd/system/agentbbs-update.timer
systemctl daemon-reload
fi
# ---- 8. move admin OpenSSH to ADMIN_SSH_PORT (before agentbbs takes :22) -----
log "moving admin OpenSSH to :${ADMIN_SSH_PORT}"
install -d -m 0755 /etc/ssh/sshd_config.d