mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +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
105
internal/sandbox/sandbox.go
Normal file
105
internal/sandbox/sandbox.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Package sandbox wraps game/agent subprocesses with per-session isolation
|
||||
// and resource limits (PRD §7 S2). It prefers bubblewrap, falls back to
|
||||
// prlimit, and degrades to a plain exec with a warning.
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Mode selects the isolation technology.
|
||||
type Mode string
|
||||
|
||||
const (
|
||||
ModeAuto Mode = "auto"
|
||||
ModeBwrap Mode = "bwrap"
|
||||
ModePrlimit Mode = "prlimit"
|
||||
ModeNone Mode = "none"
|
||||
)
|
||||
|
||||
// Limits are per-process resource caps.
|
||||
type Limits struct {
|
||||
CPUSeconds int // hard CPU-time cap (fork-bomb/runaway protection)
|
||||
MemoryMB int
|
||||
MaxProcs int
|
||||
}
|
||||
|
||||
// DefaultLimits suit a single interactive game session.
|
||||
var DefaultLimits = Limits{CPUSeconds: 3600, MemoryMB: 512, MaxProcs: 64}
|
||||
|
||||
// Runner builds sandboxed exec.Cmds.
|
||||
type Runner struct {
|
||||
mode Mode
|
||||
}
|
||||
|
||||
// New picks the best available mode when ModeAuto is requested.
|
||||
func New(mode Mode) *Runner {
|
||||
if mode == "" || mode == ModeAuto {
|
||||
switch {
|
||||
case have("bwrap"):
|
||||
mode = ModeBwrap
|
||||
case have("prlimit"):
|
||||
mode = ModePrlimit
|
||||
default:
|
||||
mode = ModeNone
|
||||
}
|
||||
}
|
||||
return &Runner{mode: mode}
|
||||
}
|
||||
|
||||
// Mode reports the active isolation mode.
|
||||
func (r *Runner) Mode() Mode { return r.mode }
|
||||
|
||||
func have(bin string) bool { _, err := exec.LookPath(bin); return err == nil }
|
||||
|
||||
// Command wraps program+args in the runner's sandbox. workDir is the only
|
||||
// writable path (savegames land there); everything else is read-only.
|
||||
func (r *Runner) Command(workDir, program string, args ...string) *exec.Cmd {
|
||||
lim := DefaultLimits
|
||||
switch r.mode {
|
||||
case ModeBwrap:
|
||||
bw := []string{
|
||||
"--ro-bind", "/", "/",
|
||||
"--dev", "/dev",
|
||||
"--proc", "/proc",
|
||||
"--tmpfs", "/tmp",
|
||||
"--bind", workDir, workDir,
|
||||
"--unshare-net",
|
||||
"--unshare-pid",
|
||||
"--die-with-parent",
|
||||
"--chdir", workDir,
|
||||
}
|
||||
// Resource caps still come from prlimit when available.
|
||||
if have("prlimit") {
|
||||
pl := prlimitArgs(lim)
|
||||
full := append(pl, "bwrap")
|
||||
full = append(full, bw...)
|
||||
full = append(full, "--", program)
|
||||
full = append(full, args...)
|
||||
return exec.Command("prlimit", full...)
|
||||
}
|
||||
full := append(bw, "--", program)
|
||||
full = append(full, args...)
|
||||
return exec.Command("bwrap", full...)
|
||||
case ModePrlimit:
|
||||
full := append(prlimitArgs(lim), program)
|
||||
full = append(full, args...)
|
||||
cmd := exec.Command("prlimit", full...)
|
||||
cmd.Dir = workDir
|
||||
return cmd
|
||||
default:
|
||||
cmd := exec.Command(program, args...)
|
||||
cmd.Dir = workDir
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
|
||||
func prlimitArgs(l Limits) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("--cpu=%d", l.CPUSeconds),
|
||||
fmt.Sprintf("--as=%d", l.MemoryMB*1024*1024),
|
||||
fmt.Sprintf("--nproc=%d", l.MaxProcs),
|
||||
"--",
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue