diff --git a/cmd/agentbbs/main.go b/cmd/agentbbs/main.go
index c65f0c7..64e87b8 100644
--- a/cmd/agentbbs/main.go
+++ b/cmd/agentbbs/main.go
@@ -53,6 +53,7 @@ import (
"github.com/profullstack/agentbbs/internal/auth"
"github.com/profullstack/agentbbs/internal/calls"
"github.com/profullstack/agentbbs/internal/chat"
+ "github.com/profullstack/agentbbs/internal/forgejo"
"github.com/profullstack/agentbbs/internal/forwardemail"
"github.com/profullstack/agentbbs/internal/games"
"github.com/profullstack/agentbbs/internal/hub"
@@ -97,6 +98,7 @@ type app struct {
sandbox *sandbox.Runner
mail mail.Config
fe forwardemail.Config // premium @bbs email provisioning
+ forgejo forgejo.Config // AgentGit git.profullstack.com account provisioning
live *liveReg // in-memory live-session registry (admin console)
gamesReg *games.Registry // AgentGames catalog
mm *games.Matchmaker // AgentGames matchmaker (agent-vs-agent)
@@ -155,6 +157,7 @@ func main() {
sandbox: sandbox.New(sandbox.Mode(env("AGENTBBS_SANDBOX", "auto"))),
mail: mail.ConfigFromEnv(),
fe: fe,
+ forgejo: forgejo.ConfigFromEnv(),
live: newLiveReg(),
dataDir: dataDir,
assets: env("AGENTBBS_ASSETS", "./assets"),
@@ -576,6 +579,7 @@ func (a *app) verifyEmailInteractive(s ssh.Session, in *bufio.Reader, u *store.U
}
if ok {
*u = vu
+ a.provisionGit(u)
wish.Println(s, " Email confirmed ✓")
return true
}
@@ -754,10 +758,30 @@ func (a *app) handleVerify(w http.ResponseWriter, r *http.Request) {
"Run ssh join@"+a.host+" to get a fresh confirmation link.")))
return
}
+ a.provisionGit(&u)
_, _ = w.Write([]byte(verifyPage("Email confirmed ✓",
"Welcome, "+u.Name+". Your account is active — ssh "+u.Name+"@"+a.host+".")))
}
+// provisionGit ensures a verified member has a git.profullstack.com account on
+// the AgentGit Forgejo backend. Every verified member gets one — free and paid
+// alike; plan only affects quotas, enforced by AgentGit, not account existence.
+// Failures are logged but never block BBS verification, and it is a no-op when
+// Forgejo is unconfigured.
+func (a *app) provisionGit(u *store.User) {
+ if u == nil || !a.forgejo.Configured() || u.Name == "" || u.Email == "" {
+ return
+ }
+ created, err := a.forgejo.EnsureUser(u.Name, u.Email)
+ if err != nil {
+ log.Error("forgejo provision", "user", u.Name, "err", err)
+ return
+ }
+ if created {
+ log.Info("provisioned git account", "user", u.Name, "host", a.forgejo.BaseURL)
+ }
+}
+
// verifyPage renders the minimal confirmation result page.
func verifyPage(title, body string) string {
return "
" + title + "" +
diff --git a/setup.sh b/setup.sh
index 1e2cb7c..14cb169 100755
--- a/setup.sh
+++ b/setup.sh
@@ -43,6 +43,12 @@ NEWS="${NEWS:-1}" # set 0 to skip the co-located Usenet/NNTP s
ERGO_VERSION="${ERGO_VERSION:-2.18.0}" # Ergo IRCd release to install
IRC_NETWORK="${IRC_NETWORK:-ProfullstackBBS}" # IRC network name shown to clients
ERGO_DATA="${ERGO_DATA:-/var/lib/ergo}" # Ergo state dir (ircd.db, tls/)
+FORGEJO="${FORGEJO:-1}" # set 0 to skip the AgentGit Forgejo backend (git.${DOMAIN#*.})
+GIT_DOMAIN="${GIT_DOMAIN:-git.${DOMAIN#*.}}" # AgentGit host (default: git., e.g. git.profullstack.com)
+FORGEJO_VERSION="${FORGEJO_VERSION:-11.0.1}" # Forgejo release to install
+FORGEJO_HTTP_ADDR="${FORGEJO_HTTP_ADDR:-127.0.0.1:3000}" # Forgejo loopback HTTP (Caddy fronts it)
+FORGEJO_DATA="${FORGEJO_DATA:-/var/lib/forgejo}" # Forgejo state dir (repos, db)
+FORGEJO_ADMIN_USER="${FORGEJO_ADMIN_USER:-agentgit-admin}" # Forgejo admin used to provision members
log() { printf '\033[1;36m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*" >&2; }
@@ -258,6 +264,13 @@ AGENTBBS_HTTP_ADDR=${HTTP_ADDR}
# AGENTBBS_FORWARDEMAIL_DOMAIN=${DOMAIN}
# AGENTBBS_WEBMAIL_URL=https://webmail.${DOMAIN}
+# AgentGit (git.profullstack.com): every verified member — free and paid alike —
+# is provisioned a Forgejo account when they confirm their email. The admin token
+# is generated and filled in by setup.sh's Forgejo section (§9d). Without it,
+# provisioning is a silent no-op. See docs (logicsrc plugins/agentgit).
+AGENTBBS_FORGEJO_URL=https://${GIT_DOMAIN}
+AGENTBBS_FORGEJO_ADMIN_TOKEN=
+
# PairUX video calls rendered as ASCII (video@ / tv@ PairUX sources):
# AGENTBBS_LIVEKIT_URL=
# AGENTBBS_LIVEKIT_KEY=
@@ -444,6 +457,19 @@ USER_LABEL_IDX=$(printf '%s' "$DOMAIN" | awk -F. '{print NF}')
# hostname (the agentbbs NNTP server reuses it for NNTPS on :563 — see §9c).
# Needs a DNS A record news.${DOMAIN} -> this host. The site itself just shows a
# connect hint; the Usenet protocol is on :563, not HTTP. Omitted when NEWS=0.
+# AgentGit: front the loopback Forgejo backend at https://${GIT_DOMAIN}. Needs a
+# DNS A record git. -> this host. Omitted when FORGEJO=0. Forgejo enforces
+# its own members-only access; agentbbs provisions the accounts (§9d).
+GIT_SITE=""
+if [ "$FORGEJO" = "1" ]; then
+ GIT_SITE="
+${GIT_DOMAIN} {
+ encode zstd gzip
+ reverse_proxy http://${FORGEJO_HTTP_ADDR}
+}
+"
+fi
+
NEWS_SITE=""
if [ "$NEWS" = "1" ]; then
NEWS_SITE="
@@ -498,7 +524,7 @@ ${DOMAIN} {
file_server
}
}
-${NEWS_SITE}
+${GIT_SITE}${NEWS_SITE}
# Free per-user homepages at .${DOMAIN} (needs wildcard DNS
# *.${DOMAIN} -> this host). On-demand TLS mints a cert only when agentbbs's
# ask endpoint confirms is a registered member, so random subdomains
@@ -723,6 +749,132 @@ else
systemctl disable --now agentbbs-news-certs.timer >/dev/null 2>&1 || true
fi
+# ---- 9d. AgentGit: Forgejo backend (https://${GIT_DOMAIN}) ------------------
+# Self-hosted Forgejo that powers AgentGit. It listens on a loopback HTTP port
+# that Caddy fronts at https://${GIT_DOMAIN} (site block in §9). Members-only:
+# open registration is disabled and sign-in is required to view, so the only way
+# in is the account agentbbs provisions for every verified member (free + paid)
+# using the admin token captured below. See logicsrc plugins/agentgit. FORGEJO=0
+# disables it.
+FORGEJO_CONF=/etc/forgejo/app.ini
+if [ "$FORGEJO" = "1" ]; then
+ log "installing Forgejo (AgentGit backend, ${GIT_DOMAIN})"
+ id -u forgejo >/dev/null 2>&1 \
+ || useradd --system --shell /usr/sbin/nologin --home-dir "$FORGEJO_DATA" --create-home forgejo
+ install -d -m 0750 -o forgejo -g forgejo \
+ "$FORGEJO_DATA" "$FORGEJO_DATA/data" "$FORGEJO_DATA/log" "$FORGEJO_DATA/repos" /etc/forgejo
+
+ if [ ! -x /usr/local/bin/forgejo ] || ! /usr/local/bin/forgejo --version 2>/dev/null | grep -q "$FORGEJO_VERSION"; then
+ case "$(uname -m)" in
+ x86_64|amd64) FJ_ARCH=amd64 ;;
+ aarch64|arm64) FJ_ARCH=arm64 ;;
+ *) FJ_ARCH="" ; warn "unknown arch $(uname -m) for Forgejo; skipping download" ;;
+ esac
+ if [ -n "$FJ_ARCH" ]; then
+ log "downloading forgejo ${FORGEJO_VERSION} (${FJ_ARCH})"
+ curl -fsSL "https://codeberg.org/forgejo/forgejo/releases/download/v${FORGEJO_VERSION}/forgejo-${FORGEJO_VERSION}-linux-${FJ_ARCH}" \
+ -o /usr/local/bin/forgejo && chmod 0755 /usr/local/bin/forgejo \
+ || warn "forgejo download failed — backend will be unavailable"
+ fi
+ fi
+
+ # app.ini is written once so Forgejo-managed secrets survive redeploys.
+ if [ ! -f "$FORGEJO_CONF" ] && [ -x /usr/local/bin/forgejo ]; then
+ FJ_SECRET_KEY=$(sudo -u forgejo /usr/local/bin/forgejo generate secret SECRET_KEY)
+ FJ_INTERNAL_TOKEN=$(sudo -u forgejo /usr/local/bin/forgejo generate secret INTERNAL_TOKEN)
+ cat > "$FORGEJO_CONF" < /etc/systemd/system/forgejo.service </dev/null 2>&1 || true
+ systemctl restart forgejo
+ sleep 2
+ systemctl is-active --quiet forgejo \
+ || warn "forgejo failed to start — check: journalctl -u forgejo -n50"
+
+ # First-run: create the admin agentbbs uses to mint member accounts, and store
+ # an admin-scoped token in agentbbs.env. Guarded on the token being empty so
+ # reruns never create duplicate tokens.
+ if ! grep -qE '^AGENTBBS_FORGEJO_ADMIN_TOKEN=.+' "$ENV_DIR/agentbbs.env" 2>/dev/null; then
+ FJ_ADMIN_PW=$(head -c32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c24)
+ sudo -u forgejo GITEA_WORK_DIR="$FORGEJO_DATA" /usr/local/bin/forgejo admin user create \
+ --admin --username "$FORGEJO_ADMIN_USER" --email "agentgit@${GIT_DOMAIN}" \
+ --password "$FJ_ADMIN_PW" --must-change-password=false --config "$FORGEJO_CONF" >/dev/null 2>&1 \
+ || true
+ FJ_TOKEN=$(sudo -u forgejo GITEA_WORK_DIR="$FORGEJO_DATA" /usr/local/bin/forgejo admin user generate-access-token \
+ --username "$FORGEJO_ADMIN_USER" --token-name "agentbbs-$(date +%s)" --scopes write:admin \
+ --config "$FORGEJO_CONF" 2>/dev/null | grep -oE '[0-9a-f]{40}' | head -1)
+ if [ -n "$FJ_TOKEN" ]; then
+ upsert_env AGENTBBS_FORGEJO_URL "https://${GIT_DOMAIN}"
+ upsert_env AGENTBBS_FORGEJO_ADMIN_TOKEN "$FJ_TOKEN"
+ log "Forgejo admin token provisioned into agentbbs.env"
+ else
+ warn "could not mint Forgejo admin token — set AGENTBBS_FORGEJO_ADMIN_TOKEN by hand (journalctl -u forgejo)"
+ fi
+ fi
+else
+ systemctl disable --now forgejo >/dev/null 2>&1 || true
+fi
+
# ---- 10. firewall + start agentbbs on :22 ----------------------------------
log "configuring firewall + starting agentbbs"
ufw allow 22/tcp >/dev/null
@@ -754,6 +906,8 @@ cat < oper password in ${ENV_DIR}/ergo-oper.txt
News news.${DOMAIN}:563 (NNTPS) newsreaders + agents ${NEWS:+(set NEWS=0 to disable)}
ssh -t news@${DOMAIN} the in-BBS newsreader (DNS: news.${DOMAIN} A -> host)
+ AgentGit https://${GIT_DOMAIN} git for members (auto-account on email verify) ${FORGEJO:+(set FORGEJO=0 to disable)}
+ DNS: ${GIT_DOMAIN} A -> this host
Config ${ENV_DIR}/agentbbs.env (set CoinPay + LiveKit, then: systemctl restart agentbbs)
Logs journalctl -u agentbbs -f (IRC: journalctl -u ergo -f)