mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
AgentBBS: M0 core hub, M1 arcade, pods with CoinPay membership
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>
This commit is contained in:
commit
f3b085a08f
21 changed files with 2405 additions and 0 deletions
63
plugins/arcade/ptyexec.go
Normal file
63
plugins/arcade/ptyexec.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package arcade
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// ptyExec is a tea.ExecCommand that runs the child on a real host PTY and
|
||||
// bridges it to the session streams. Needed because doom-ascii (like most
|
||||
// raw-mode terminal programs) demands an actual TTY, and over SSH the
|
||||
// bubbletea program's stdin/stdout are session streams, not a host TTY.
|
||||
type ptyExec struct {
|
||||
cmd *exec.Cmd
|
||||
stdin io.Reader
|
||||
stdout io.Writer
|
||||
width, height int
|
||||
}
|
||||
|
||||
func newPtyExec(cmd *exec.Cmd, width, height int) *ptyExec {
|
||||
return &ptyExec{cmd: cmd, width: width, height: height}
|
||||
}
|
||||
|
||||
func (p *ptyExec) SetStdin(r io.Reader) { p.stdin = r }
|
||||
func (p *ptyExec) SetStdout(w io.Writer) { p.stdout = w }
|
||||
func (p *ptyExec) SetStderr(io.Writer) {}
|
||||
|
||||
var _ tea.ExecCommand = (*ptyExec)(nil)
|
||||
|
||||
func (p *ptyExec) Run() error {
|
||||
f, err := pty.StartWithSize(p.cmd, &pty.Winsize{
|
||||
Rows: uint16(max(p.height, 24)),
|
||||
Cols: uint16(max(p.width, 80)),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() { _, _ = io.Copy(p.stdout, f); close(done) }()
|
||||
go func() {
|
||||
_, _ = io.Copy(f, p.stdin)
|
||||
// Session input is gone (disconnect): don't leave the game orphaned
|
||||
// on the host (PRD §7 S3 — abandoned sessions are reaped).
|
||||
if p.cmd.Process != nil {
|
||||
_ = p.cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
err = p.cmd.Wait()
|
||||
<-done
|
||||
return err
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue