Use profullstack.com/motd as the shared Message of the Day across both
the SSH BBS hub and the Ergo IRC server.
- internal/motd: fetch + in-memory cache with background refresh (stdlib
only); Current() never blocks session start, keeps last value on error.
Source overridable via AGENTBBS_MOTD_URL.
- hub: append the daily MOTD below the existing welcome/onboarding text.
- IRC: deploy/ergo/refresh-motd.sh pulls /motd into Ergo's ergo.motd and
rehashes; setup.sh installs it + an ergo-motd.timer (hourly) mirroring
the ergo-certs timer, with a seeded fallback if the source is offline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
offerPremium ran on every join@ and called CreatePremiumCharge just to
show the pitch, so CoinPay minted a $99 payment for everyone who
connected. Show the pitch with no charge, then create the payment only
when the member types "yes" at the prompt (the SSH equivalent of
clicking "Become a paid member").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mail Caddy route (mail.<host>) and the entire §9e Mailu provisioning block
were gated on $MAIL, but MAIL is a standard env var (the mail-spool path) that
PAM's pam_mail sets for the sudo session the CI deploy runs under. So deploys saw
MAIL=/var/mail/root (!= "1"), silently skipping: the mail.<host> Caddy block
(webmail went down with TLS handshake aborts), the AGENTBBS_MAIL_SMTP_* env
upserts (incl. the new SMTP_SERVERNAME), AND it ran the else-branch which disabled
the mail cert-refresh timer every deploy. Rename the toggle to MAIL_STACK (no
collision) so all of it actually runs. Matches the intermittent 'mail block
dropped after a deploy' seen before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chat.profullstack.com Caddy block was only ever appended manually, so every
setup.sh deploy (which rewrites the whole Caddyfile) silently dropped it and
broke https://chat.profullstack.com (fell into the on-demand-TLS catch-all whose
ask endpoint denies it -> TLS handshake aborts). Generate it like the mail/news/
files sites (CHAT=1 toggle, CHAT_DOMAIN/CHAT_ADDR). The Lounge container is still
provisioned out-of-band; this only owns the route.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AgentMail compose/send failed with 'cannot validate certificate for 127.0.0.1
because it doesn't contain any IP SANs': the sender dialed the local relay at
127.0.0.1:25 and net/smtp pinned the TLS ServerName to the dial host, but the
relay's cert is for mail.<host>. Reimplement smtpSend (mirrors net/smtp.SendMail)
with an overridable IMAPConfig.SMTPServerName; default it to the mail host
(AGENTBBS_MAIL_SMTP_SERVERNAME). Now we dial the loopback for relay permission
yet verify the real hostname cert — no /etc/hosts hack. setup.sh upserts the new
var. Tested against a fake SMTP server (full MAIL/RCPT/DATA flow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auth model change: ergo-auth-member now verifies the SASL passphrase against a
per-member pbkdf2 hash (/var/lib/ergo/irc-passwd) in addition to BBS membership,
replacing the old "membership is the credential, passphrase ignored" gate that
let anyone who knew a member name connect as them. Rewrote deploy/ergo/auth-script.sh
in python3 (drops jq/curl dep); setup.sh already installs it as ergo-auth-member.
- scripts/set-irc-password.sh: set/rotate a member's IRC password (or --all to
backfill); also syncs The Lounge saslPassword so the web client keeps working.
- setup.sh landing page: new "IRC from a desktop client" section with irssi/HexChat/
WeeChat SASL setup (connect by network name ProfullstackBBS, not hostname).
- docs/irc.md: document password auth, the helper, the 6697 cloud-firewall note,
and an irssi quick-start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a browser-based file manager so members can use their files without an
SSH key. Served on a loopback HTTP server (AGENTBBS_FILES_WEB_ADDR, default
127.0.0.1:8092) that Caddy fronts at files.<host>. Members sign in with their
webmail username + password, verified against the Mailu IMAP backend
(mailbox.VerifyLogin), and browse the same virtual /me + /public areas as SFTP
— no home directory is ever exposed. Upload/download/mkdir/delete with the
private-area quota enforced; reuses internal/files confinement (fs.go).
setup.sh renders the files.<DOMAIN> Caddy site + env knob. Unit tests cover
the auth gate and an upload/list/download/delete round trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interactive reader (ssh mail@ / hub Mail) was read-only; the send path
existed only for agents (bot mode). Add compose (c), reply (r), reply-all (a)
to the TUI with a To/Cc/Subject/Body form (tab/arrows to move fields, ctrl+d
send, esc cancel). Reply prefills + quotes the original and threads via
In-Reply-To. Tests drive the model key-by-key through send.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WorkingDirectory is /opt/ergo with ProtectSystem=strict (only /var/lib/ergo
and /etc/ergo are writable), but lock-file was relative ("ircd.lock"), so Ergo
tried to create /opt/ergo/ircd.lock on a read-only path and crash-looped with
"failed to acquire flock on ircd.lock: read-only file system" — nothing ever
listened on 6697/6667/8097 (clients got ECONNREFUSED). datastore.path is already
absolute (__DATA__/ircd.db); per Ergo's own config comment the lock-file should
match. Render it to __DATA__/ircd.lock (/var/lib/ergo, ergo-owned + in
ReadWritePaths).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New docs/credentials.md covers member git-account provisioning, the
notify-creds backfill command (flags + behavior), required env, and the
two SMTP paths — transactional AGENTBBS_SMTP_* (STARTTLS :587) vs. the
gateway loopback relay AGENTBBS_MAIL_SMTP_ADDR (:25 into Mailu),
clarifying why :25 is correct and that 465 (not "467") is implicit-TLS.
Cross-linked from README ops and docs/mail.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(files): SFTP member storage — private workspaces + shared public area + mgmt TUI
Implements M4 (Files). A fully virtual Go SFTP server (pkg/sftp + crypto/ssh,
no OS users) wired as an "sftp" subsystem on the existing :22 wish listener, so
members reach their files with their login key:
sftp files@bbs.profullstack.com # scp/rsync ride the same endpoint
Identity is the SSH key (the username is conventional/ignored). Two areas per
session: a private, quota-limited /me workspace and a single shared public file
area /public (old-school BBS file area; world-read, members-only write by
default, operator-moderated). This reverses the old NG1 "no sharing" boundary in
favour of one sanctioned, inspectable sharing surface (PRD §9.3 amended).
internal/files:
- backend.go service, layout, quota/usage, live-session registry, operator API
- fs.go per-session virtual FS; resolve() is the single security
chokepoint (area confinement + symlink-escape guard) + pkg/sftp
request handlers
- server.go subsystem handler: key auth -> member session -> request server,
with byte metering and force-disconnect
- tui.go in-BBS member browser (hub plugin "Files")
- admin.go operator management TUI: sessions, workspaces/quotas, public area
Operator console: ssh sftp@<host> (allowlist-gated; sftpadmin@/filesadmin@
aliases) — list/disconnect sessions, set per-user quotas, revoke SFTP access,
toggle public write, moderate the public area.
store: files_access (per-user quota override + revoked) and files_settings
(public-write mode) tables + methods. main.go wiring guarded by AGENTBBS_FILES
(+ AGENTBBS_FILES_QUOTA_MB, default 1 GiB). Route names reserved.
Tests (incl -race): path traversal/confinement, symlink-escape rejection,
public-write ACL, quota enforcement, usage accounting, and an end-to-end run
against a real SFTP client. Docs: docs/files.md; PRD §5.3/§5.3.1/§9.3 + README
updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Add notify-creds subcommand to (re)email members git + mailbox creds
`agentbbs notify-creds` backfills credential emails to verified members
who signed up before the git/mailbox welcome emails existed.
- git (all verified): forgejo.EnsureUserReset resets each account to a
fresh one-time password (must-change) and emails the web login link,
username, and password. New method since the original one-time
password is not recoverable for existing accounts.
- mailbox (all verified): ensures the forwardemail alias and emails the
address + webmail link.
- Preview by default; --send executes. --git/--mail/--user filters.
Refuses --send without SMTP; warns+skips when Forgejo/forwardemail
are unconfigured.
Also folds in the welcome-email functions (gitWelcomeEmailBody,
mailWelcomeEmailBody, EnsureUser password return, provisionGit/
ensurePremium sends) that this builds on. README ops + forgejo tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(mail): give every verified member a free @bbs.profullstack.com mailbox
Email was built but paid-only (Founding Lifetime gate) and never wired to a
running backend. Make it a free benefit of membership and split the address
domain from the mail-server host.
- internal/mailu: Mailu admin-API client; EnsureUser idempotently provisions a
mailbox via the loopback admin REST API (token = mailu.env API_TOKEN).
- main.go: auto-provision <name>@<mailDomain> at join@ verification and on first
Mail open; un-gate the Mail hub entry + mail@ (membership/email-verified, not
Premium); address domain (AGENTBBS_MAIL_ADDR_DOMAIN, default the BBS host) is
now distinct from the mail server host (AGENTBBS_MAIL_DOMAIN) and the webmail
URL. Drop the forwardemail alias path (Mailu now owns delivery for everyone).
- mailbox: gate on membership (a registered handle) instead of Paid;
ErrNotPaid -> ErrNotMember.
- join@ copy: list email under free membership; premium now pitches custom
domains + Tor only.
- setup.sh / docs/mail.md / deploy/mailu: address-domain vs server-host split,
Mailu API token, MX for the address domain, local-relay SMTP for verify codes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(mailu): pin Docker network subnet to match SUBNET; ignore runtime state
The base compose declares no network, so Docker assigns the default bridge an
arbitrary subnet that won't match mailu.env SUBNET — breaking Mailu's internal
service auth/relay. Add a docker-compose.override.yml.example that pins the
default network to 192.168.203.0/24, and gitignore the live override + Mailu
runtime state (mailu.env, certs/, data/).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(mail): plaintext loopback IMAP so the gateway bypasses Mailu's front
Mailu's front (nginx mail proxy) pre-authenticates against Mailu's user DB before
proxying to Dovecot, which rejects the Dovecot master-user login <addr>*gateway.
The gateway must reach Dovecot directly. The imap container has no TLS cert (only
the front does), so the bypass is plaintext over loopback — the master password
never leaves the host.
- mailbox: IMAPConfig.Plaintext dials with DialInsecure (loopback only).
- main.go: mailClientFor sets Plaintext from AGENTBBS_MAIL_IMAP_PLAINTEXT.
- override.example: add the unbound resolver (admin needs DNSSEC), webmail image
fix (2024.06 uses mailu/webmail), and publish Dovecot 143 on 127.0.0.1:14143.
- docs/mail.md: document the front-bypass, the dovecot.conf master passdb (Mailu
includes that exact filename), and the 644 master-users perms (640 = temp_fail).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* deploy(mailu): wire gateway IMAP to the loopback Dovecot path in setup.sh
setup.sh §9e set AGENTBBS_MAIL_IMAP_ADDR to the front's :993, which the front's
auth proxy rejects for the master-user login (and would clobber the working
loopback wiring on every self-update). Point it at 127.0.0.1:14143 +
AGENTBBS_MAIL_IMAP_PLAINTEXT=1 instead, matching the override + docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(mail): give free members a webmail password at join@
The gateway opens mailboxes via the Dovecot master user (no member password),
but webmail (Roundcube) needs the member to have a password. join@ now sets a
fresh, readable webmail password via the Mailu API and shows it with the webmail
URL + login, so free members can use webmail at mail.profullstack.com.
- mailu: SetPassword (PATCH /user/<email> raw_password) + test.
- main.go: setWebmailPassword + readablePassword; join@ displays url/login/password.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
`agentbbs notify-creds` backfills credential emails to verified members
who signed up before the git/mailbox welcome emails existed.
- git (all verified): forgejo.EnsureUserReset resets each account to a
fresh one-time password (must-change) and emails the web login link,
username, and password. New method since the original one-time
password is not recoverable for existing accounts.
- mailbox (all verified): ensures the forwardemail alias and emails the
address + webmail link.
- Preview by default; --send executes. --git/--mail/--user filters.
Refuses --send without SMTP; warns+skips when Forgejo/forwardemail
are unconfigured.
Also folds in the welcome-email functions (gitWelcomeEmailBody,
mailWelcomeEmailBody, EnsureUser password return, provisionGit/
ensurePremium sends) that this builds on. README ops + forgejo tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements M4 (Files). A fully virtual Go SFTP server (pkg/sftp + crypto/ssh,
no OS users) wired as an "sftp" subsystem on the existing :22 wish listener, so
members reach their files with their login key:
sftp files@bbs.profullstack.com # scp/rsync ride the same endpoint
Identity is the SSH key (the username is conventional/ignored). Two areas per
session: a private, quota-limited /me workspace and a single shared public file
area /public (old-school BBS file area; world-read, members-only write by
default, operator-moderated). This reverses the old NG1 "no sharing" boundary in
favour of one sanctioned, inspectable sharing surface (PRD §9.3 amended).
internal/files:
- backend.go service, layout, quota/usage, live-session registry, operator API
- fs.go per-session virtual FS; resolve() is the single security
chokepoint (area confinement + symlink-escape guard) + pkg/sftp
request handlers
- server.go subsystem handler: key auth -> member session -> request server,
with byte metering and force-disconnect
- tui.go in-BBS member browser (hub plugin "Files")
- admin.go operator management TUI: sessions, workspaces/quotas, public area
Operator console: ssh sftp@<host> (allowlist-gated; sftpadmin@/filesadmin@
aliases) — list/disconnect sessions, set per-user quotas, revoke SFTP access,
toggle public write, moderate the public area.
store: files_access (per-user quota override + revoked) and files_settings
(public-write mode) tables + methods. main.go wiring guarded by AGENTBBS_FILES
(+ AGENTBBS_FILES_QUOTA_MB, default 1 GiB). Route names reserved.
Tests (incl -race): path traversal/confinement, symlink-escape rejection,
public-write ACL, quota enforcement, usage accounting, and an end-to-end run
against a real SFTP client. Docs: docs/files.md; PRD §5.3/§5.3.1/§9.3 + README
updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The deploy's rootless-podman context intermittently fails (pre-pull/build/even
image-exists), which made setup.sh upsert AGENTBBS_POD_IMAGE back to the base
ubuntu and silently strip Claude Code/Codex from pods. Only upsert when we
actually have localhost/agentbbs-pod:latest; otherwise leave the configured
value untouched (the agentbbs daemon uses the local image from its own session).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Code in your pod and push to git.profullstack.com using the SAME SSH key you
signed in with — nothing is copied into the pod. When a member attaches with
agent forwarding (ssh -A), agentbbs listens on a fresh unix socket in a
per-user agent dir bind-mounted at /run/agentbbs-agent and proxies it back over
the session; the pod shell gets SSH_AUTH_SOCK pointed at it. The pod image's
ssh_config sends git@git.profullstack.com to Forgejo's SSH server (:2222), so
`git clone git@git.profullstack.com:you/repo.git` just works.
- pods.go: agentDir + startAgent (per-session socket, cleaned up on exit);
Attach injects SSH_AUTH_SOCK when ssh.AgentRequested; ensure() bind-mounts the
agent dir and self-heals idle pods missing it. No main.go change needed —
charmbracelet/ssh sets AgentRequested from the session request loop.
- pods/Containerfile: /etc/ssh/ssh_config.d entry (port 2222, user git,
accept-new) so the conventional git@ URL reaches Forgejo.
- setup.sh: keep using an already-built pod image if a later rebuild
transient-fails, so a flaky deploy never downgrades pods to the base image.
Build/vet/test/gofmt clean. Image rebuilt on the host; `ssh -G
git.profullstack.com` resolves to port 2222 / user git. End-to-end push needs a
live `ssh -A` session (validate after deploy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the stub site root with a full page: a short "what's a BBS?" history
(1980s dial-up boards, SysOps, door games, FidoNet/Usenet) and the complete
command list — join/bbs/NAME/pod/mail/news/irc/game/domain over SSH — plus the
web services (AgentGit profiles, IRC, member homepages) and the git
"membership is your account" push flow. Regenerated on every run (templated
from $DOMAIN/$GIT_DOMAIN/$IRC_DOMAIN) so it stays current as features land.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Members can now code in their pod: build a custom pod image (FROM the base
Ubuntu) that ships git, openssh-client, Node.js 22, and the Claude Code
(`claude`) and Codex (`codex`) CLIs. BYO key — no credentials are baked in; a
member exports their own ANTHROPIC_API_KEY / OPENAI_API_KEY (or uses the tools'
login flow), stored in their persisted home.
- pods/Containerfile: the image (also drops a BYO-key + git-push login hint).
- setup.sh: build it on the host (rootless podman, layer-cached), switch
AGENTBBS_POD_IMAGE to localhost/agentbbs-pod:latest (upserted for existing
installs), keeping the base image if the build fails.
- pods.go: image-aware self-heal — an idle pod on an out-of-date image is
recreated (home volume kept) so the new tooling rolls out without a manual
rebuild and without disturbing active sessions.
Verified: image builds on the host; inside it node v22, git, ssh, `claude
--version` (2.1.186) and `codex --version` (0.142.0) all run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make git.profullstack.com a real, key-authenticated git host for every BBS
member ("BBS membership is the git account", SSH-key auth end to end):
- forgejo.EnsureKey: register a member's SSH public key on their Forgejo
account (idempotent, ignores the key comment). So the key they sign in to
the BBS with is also their git push key.
- provisionGit now takes the session public key and registers it after
ensuring the account; called on email verification AND (newly) on every
member login, so members who predate AgentGit — or whose key wasn't
registered yet — are backfilled automatically and off the hot path.
- setup.sh:
- admin token scopes write:admin,read:user,write:user (the old write:admin
alone failed userExists' /users lookup, so provisioning never worked).
- REQUIRE_SIGNIN_VIEW=false so member profiles + public repos are viewable
at git.profullstack.com/<name> (private repos stay private; accounts are
still created only by agentbbs).
- Enable Forgejo's built-in SSH server (port 2222, BUILTIN_SSH_SERVER_USER=git)
and open the firewall, so members push to git@git.profullstack.com:2222.
Verified live: all members provisioned, git.profullstack.com/chovy serves the
profile, and a push over ssh://git@host:2222 with a registered key succeeds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hangman joins Snake as a built-in, leaderboard-backed TUI game (PRD §5.1).
Endless mode: each solved word banks points (longer words and unused guesses
score more) and deals a fresh word with full lives; the run ends when one word
exhausts all six wrong guesses, persisting the total for members under the
"hangman" score key. Guests play without persisting, same as Snake.
Generalize the leaderboard board to take a game name and split the single
"Leaderboard" row into per-game "Leaderboard — Snake" / "Leaderboard — Hangman"
entries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GameNames ("game", "games") are reserved SSH route usernames for
AgentGames but IsReservedName did not check GameNames, allowing
members to register accounts that collide with SSH routes.
Fixes#40
Co-authored-by: root <root@vultr.guest>
Adds Carrier-Grade NAT (100.64.0.0/10), benchmarking (198.18.0.0/15),
and documentation/example (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24)
ranges to the isBlockedIP check.
Go's net.IsPrivate() covers RFC1918 but not these shared/reserved ranges.
Fixes#43
Co-authored-by: root <root@vultr.guest>
parseRange previously returned (0, MaxInt64) for unparseable input,
causing OVER/XOVER to deliver the full article overview instead of
returning an empty result. Now returns (0, 0) for any parse error.
Fixes#45
Co-authored-by: root <root@vultr.guest>
The in-BBS ssh irc@ route no longer exists — internal/auth only reserves
'irc' as a name but has no handler for it. docs/irc.md already documents
this correctly.
- Removed ssh -t irc@ command and 'built-in IRC client' language
- Updated to reflect that members connect with their own client
- Kept correct native TLS and WebSocket connection examples
Co-authored-by: threebeats <threebeats@users.noreply.github.com>
The 80s arcade classics (Space Invaders/nInvaders, Pac-Man/pacman4console,
Tetris/tint, Moon Patrol/moon-buggy) are ncurses programs: initscr() fails
with "Error opening terminal" when TERM is unset. Game subprocesses were
built with exec.Command and no Env, so they inherited the agentbbs systemd
daemon's environment — which has no TERM — and every game exited before
drawing a frame. DOOM was unaffected because doom-ascii writes ANSI directly
and never touches terminfo.
Thread the client PTY's TERM through plugin.Context and hand each sandboxed
game a curated environment (TERM, PATH, HOME, LANG=C.UTF-8) instead of the
daemon's. Curating the env also stops leaking operator secrets (e.g.
COINPAY_API_KEY) into third-party game binaries.
Verified live on bbs.profullstack.com: Space Invaders and Pac-Man now render;
previously all four died instantly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rootless-podman default capability set (already on main) lets apt drop
to the _apt user on its own, so disabling the apt download sandbox via
tuneApt is dead code. Removing it; apt now works the proper way.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(pods/admin): root admin alias, default-caps cleanup, pod rebuild script
- auth: add `root` as an admin-console route alias (alongside admin/sysop);
still gated by $AGENTBBS_ADMINS — the name confers nothing on its own.
- pods: drop the now-redundant tuneApt apt-sandbox hack. Rootless podman keeps
its default capability set, so apt/chown/su work without disabling the
download sandbox.
- scripts/rebuild-pods.sh: recreate all member pods (keeps home volumes) so
they pick up the current container profile on next `ssh pod@`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(arcade,ui): 80s arcade classics + shared menu theme
Arcade games (PRD §5.1), generalizing the sandboxed-PTY DOOM path into an
external-game registry: Space Invaders (nInvaders), Pac-Man (pacman4console),
Tetris (tint/vitetris), Moon Patrol (moon-buggy). Binaries resolve from
assets/bin, PATH, then /usr/games, so a distro install or a hand-built binary
lights each game up; missing games are skipped with a discovery hint. Installed
on the host via `scripts/fetch-assets.sh --arcade` (apt), wired into setup.sh
behind FETCH_ARCADE (default on).
UI: new shared ui.Theme.MenuItem widget (accent cursor + badge, description
shown only for the focused row) adopted by the hub and arcade menus; the hub
groups rows under Features/Sessions headers and the arcade under
DOOM/ARCADE/BUILT-IN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(deploy): build Go binaries on the runner, ship them, SKIP_BUILD on box
The deploy SSHed into the ~458MB droplet and ran `go build` there. The Go
linker's peak memory OOM-killed the build — and with it the sshd serving the
deploy session — surfacing as "Connection closed by remote host" (exit 255).
It was flaky because it tracked momentary memory pressure from the co-resident
ergo/forgejo/tor/podman/agentbbs processes (run #25 passed, #26 failed on
near-identical code).
Build both binaries on the 16GB GitHub runner instead (pure-Go, modernc
sqlite, so CGO_ENABLED=0 static cross-build), scp them to the droplet, and run
setup.sh with SKIP_BUILD=1 so the box never compiles. Arch is detected from
the droplet so amd64/arm64 both work. setup.sh now also skips the Go toolchain
download when SKIP_BUILD=1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(members): member directory + store-and-forward messaging
A members-only hub plugin (the BBS "who") plus user-to-user messaging:
- internal/store: messages table + SendMessage/Inbox/UnreadCount/MarkRead, and
OnlineUsers (open sessions) for presence. MarkRead is recipient-scoped so a
member can only clear their own mail.
- plugins/members: directory with online dots + last-seen, a finger-style
profile view, a minimal compose box, and an inbox that marks read on open.
- ssh msg@host <user> [text]: scriptable CLI to leave a note (body from args or
stdin), mirroring the existing finger route; "msg"/"message" are reserved.
- hub: "N unread" badge on login (hubMOTD). plugin.Context gains Host for member
homepage URLs.
Extends the existing finger@ behavior (ssh <name>@host) rather than replacing it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The deploy SSHed into the ~458MB droplet and ran `go build` there. The Go
linker's peak memory OOM-killed the build — and with it the sshd serving the
deploy session — surfacing as "Connection closed by remote host" (exit 255).
It was flaky because it tracked momentary memory pressure from the co-resident
ergo/forgejo/tor/podman/agentbbs processes (run #25 passed, #26 failed on
near-identical code).
Build both binaries on the 16GB GitHub runner instead (pure-Go, modernc
sqlite, so CGO_ENABLED=0 static cross-build), scp them to the droplet, and run
setup.sh with SKIP_BUILD=1 so the box never compiles. Arch is detected from
the droplet so amd64/arm64 both work. setup.sh now also skips the Go toolchain
download when SKIP_BUILD=1.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The box auto-deploys (self-update timer pulls origin/main, rebuilds, restarts
agentbbs), but `systemctl restart agentbbs` only restarts the daemon — it never
touches the long-lived per-user pod containers. ensure() also short-circuits on
any container that already exists, so pods created before the homepage bind
landed would never gain the /home/dev/public_html mount without a manual
`podman rm`. That defeats the "everything happens automatically on push" goal.
Make ensure() self-healing: when a pod exists but lacks the public_html mount,
recreate it so the bind is applied. The named home volume survives `rm`, so the
member's files are kept. Only heal when the pod is idle (attached count 0) to
avoid pulling a running pod out from under an active session — an unbound pod
heals on its next idle attach. New hasMount() inspects the container's mounts.
Net effect: push to main -> self-update redeploys within the timer interval ->
the next `ssh pod@` recreates the pod with the bind. No manual step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A member's pod home (/home/dev) is a named container volume, but Caddy serves
<name>.<host> from the host path <data>/users/<name>/public_html. The two were
disconnected, so editing ~/public_html/index.html in the pod never changed the
served page — contradicting the on-screen "edit ~/public_html to make it yours"
instruction.
Bind-mount <data>/users/<user>/public_html at /home/dev/public_html when pods
start. The host tree is created if absent so the mount source exists, and under
the docker fallback (uid 1000) the one-shot init container now also chowns the
bind path; rootless podman maps container root to the host service user that
already owns the tree, so no chown is needed there.
Detect now takes the host users dir; pass "" to disable the bind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>