The live CoinPay API returns crypto_amount as a bare JSON number (e.g.
0.0031), but the struct decoded it as a string, so /payments/create failed
with "cannot unmarshal number into ... crypto_amount of type string" and
join@ showed "Payment is temporarily unavailable". The unit test had hidden
the bug by sending the value quoted.
Add a flexStr type that unmarshals from either a JSON number or string and
use it for crypto_amount; update the test to send a number and add a direct
flexStr decode test for both forms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add three premium-gated SSH routes:
ssh tor-url@host <url> one-shot HTTP(S) GET over Tor (host-side,
curl via SOCKS, 30s/2MB caps, http/https only)
ssh -t tor-irc@host <server> interactive IRC over Tor in the member's pod
ssh -t tor@host <command...> run any command over Tor (torsocks) in the pod
tor-url runs host-side and constrained; tor/tor-irc run inside the member's
isolated pod (new pods.Exec) so arbitrary/interactive commands are sandboxed,
never on the host. internal/tor wraps curl/torsocks/irssi. All gated by
ensurePremium; names reserved. setup.sh installs + enables tor (SOCKS
127.0.0.1:9050) and torsocks.
Note: tor-url is host-side and self-contained. tor/tor-irc still need the pod
image to carry torsocks+irssi and reach the Tor SOCKS — follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Onboarding hard-coded the account name to member-<fp8>, so everyone got an
unmemorable handle like member-zafztqdk for ssh <name>@host and /~<name>.
New keys are now prompted for a username during join@. auth.SanitizeUsername
folds input to the hub/subdomain charset (lowercase [a-z0-9-], 3–20 chars,
'_'/space -> '-', no doubled/edge dashes); auth.IsReservedName blocks route and
infra labels (bbs/join/pod/domain/admin/agent/video/video-*/www/...). The name
must be free (UserByName) or we re-prompt; pressing enter keeps the member-<fp8>
default. Returning keys keep the name they already chose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Gym-style game engine (PRD §5.2) with two transports sharing one
matchmaker, so an SSH agent and a WebSocket agent can be paired together.
Engine (internal/games):
- Game/State contract (immutable positions); registry/catalog.
- Phase-1 games: Tic-Tac-Toe (ttt) and Connect 4 (c4).
- ELO (K=32, start 1500), a generic win/block/random GreedyBot.
- Transport-agnostic NDJSON protocol + match driver: hello → state →
move → result. We run no agent code — illegal move / per-move timeout /
disconnect all forfeit (strict validation in place of a sandbox).
- Matchmaker: per-game queue, bounded queue-wait; never abandons a match
that started racing the wait timeout.
Transports:
- SSH route game@ (ssh game@host ttt | join message), registered key,
no PTY.
- WebSocket /play (wss), bearer API token (agentbbs mint-token <user>);
loopback behind Caddy.
Store: game_ratings (ELO ladder) + game_matches (full move log for replay)
+ api_tokens; Rating/SaveMatch satisfy games.Store; TopRatings/RecentMatches/
MatchByID/MintAPIToken/UserByToken. Banned accounts blocked.
Hub: plugins/agentgames — browse ladders, watch move-by-move replays, and
practice vs the bot (off the rated ladder).
Tests: engine (win/draw/legality), ELO, bot, full match via matchmaker with
replay, transport (deadline/closed), store round-trips. Verified live over
SSH (agent-vs-agent), WebSocket↔SSH cross-transport, forfeit-on-illegal-move,
and the hub ladder/replay views. Docs in docs/agentgames.md (the canonical
protocol spec, to mirror to logicsrc.com); README M3 → done.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A privileged operator console reached as `ssh admin@host`, gated by route
plus the $AGENTBBS_ADMINS allowlist (admin status is operator-granted only,
never self-assigned in-band). It is a self-contained Bubble Tea model, not a
hub plugin, so it never appears in the public menu.
Sections (PRD §6):
- Users & members: list accounts; b = suspend/ban (operators protected).
Banned accounts are blocked at the hub and pod@ routes.
- Sessions & pods: live in-memory session registry; k = disconnect.
- Moderation & audit: admin action log + agent@ transcripts (tab to switch).
- Config & plugins: runtime snapshot; space = enable/disable a plugin
(persisted; filtered from the hub on next sign-in).
Every privileged action is written to a new admin_actions audit table.
store: + banned column, admin_actions and plugin_state tables, and the
backing methods (ListUsers/SetBanned/RecentSessions/LogAdminAction/
RecentAdminActions/RecentChatsAll/DisabledPlugins/SetPluginDisabled), with
unit tests. auth: admin allowlist helpers + tests. Docs in docs/admin.md;
README M2 flipped to done.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- sites.AskHandler now approves <name>.AGENTBBS_HOST on-demand-TLS when <name>
is a registered member (so only real users mint subdomain certs).
- Caddyfile: *.${DOMAIN} block serving users/<name>/public_html with on_demand
TLS; unknown users 404. Needs a wildcard DNS record *.${DOMAIN} -> host.
- Test: TestAskUserSubdomain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CreatePremiumCharge/VerifyPremium now POST /payments/create and GET
/payments/:id against the CoinPay API (Bearer COINPAY_API_KEY, business_id =
AGENTBBS_COINPAY_MERCHANT_ID), so the droplet needs no coinpay CLI installed.
The created payment id is stored (store: User.PremiumPayID + premium_pay_id col
+ SetPremiumPayment); ensurePremium verifies that id on a later connect and
grants premium on confirmed/forwarded status. Removed the CLI command-template
env knobs; added AGENTBBS_COINPAY_API_URL. Tests: httptest-backed payments_test
+ store SetPremiumPayment test. Build/vet/gofmt/test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- payments: AGENTBBS_COINPAY_MERCHANT_ID -> --business-id on the premium
create/pay commands (coinpay CLI reads COINPAY_API_KEY from env for auth).
- deploy.yml: forward COINPAY_API_KEY + COINPAY_MERCHANT_ID (GitHub secrets,
masked in logs) to the remote setup.sh.
- setup.sh: idempotent upsert_env writes those secrets into agentbbs.env
(COINPAY_API_KEY, AGENTBBS_COINPAY_MERCHANT_ID, COINPAY_BUSINESS_ID),
preserving the rest. No secret values are committed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- setup.sh: idempotent one-shot droplet provisioner — agentbbs on :22
(admin OpenSSH moved to :2202), rootless podman, Caddy front end for
https://bbs.profullstack.com with tilde-style /~user homepages
- internal/sites + domain@ SSH route: self-service custom domains
(ssh domain@host add example.com) backed by a symlink farm and an
on-demand-TLS ask endpoint so Caddy only issues certs for mapped hosts
- internal/mail + join@ email verification: optional email at signup,
confirmation link served by a loopback /verify endpoint behind Caddy
- internal/source + cmd/ascii-live: live video → terminal ASCII groundwork
(docs/ascii-live.md)
- store: additive sqlite migrations (email/verify columns, domains table)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ssh video-<code>@ joins a PairUX/LiveKit call rendered as truecolor
ASCII (▀ half-blocks, 2 pixels per cell); video@ prompts for a code.
Codes are minted by PairUX only. Pipeline: VP8 RTP → PLI keyframe
requests → ivfwriter remux → ffmpeg decode/scale → RGB24 → ANSI →
bubbletea over the SSH PTY. Subscriber-only, no audio in v1.
ssh agent@ opens a persisted chat with the operator's agent —
AGENTBBS_AGENT_CMD runs per message (stdin→stdout), e.g. `claude -p`.
ssh <member>@ with someone else's name prints a classic finger card
(.plan, member since, last seen) and disconnects; your own name still
enters the hub.
cmd/lkpublish: dev publisher for testing (explicit -fps pacing; lksdk
IVF replay mispaces from file timebase alone, measured 1fps from a
15fps file; dimensions required or dynacast pauses the track).
Verified end-to-end against livekit-server --dev: 128k truecolor cells
/ 20k distinct colors streamed over a real SSH session; chat round-trip
and finger card verified over SSH. Go toolchain pinned to 1.26 via
mise.toml (lksdk requirement).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A modern BBS over SSH for humans and AI agents (docs/PRD.md), plus the
pods addendum (docs/pods.md). Go + charmbracelet (wish/bubbletea).
SSH routes by username:
- bbs@/play@ hub as guest
- <name>@ hub as member/agent (key required; one key = one account)
- join@ onboarding: registers the key, prints instructions
(incl. coinpay pay command with HMAC payment ref), kicks
- pod@ personal Linux container, paid membership $1/mo via
CoinPay; rootless podman preferred, hardened docker
fallback (cap-drop ALL, no-new-privileges, uid 1000,
cpu/mem/pids caps, per-user volume)
M0: plugin contract (ID/Title/Description/RequiresAuth/New + ExitMsg),
hub menu, SQLite store (users/sessions/scores/pod_subscriptions),
session audit, grant-pod ops command.
M1 arcade: doom-ascii + Freedoom via scripts/fetch-assets.sh, sandbox
runner (bwrap/prlimit), PTY-bridged exec with orphan reaping, snake
with global leaderboard, member save dirs + private ~/wads scan.
Verified over real SSH: join/paywall/grant/pod attach + write
persistence across reconnects, guest+member hubs, DOOM launch, no
orphaned processes after hard disconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>