mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
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>
105 lines
2.6 KiB
Go
105 lines
2.6 KiB
Go
// 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),
|
|
"--",
|
|
}
|
|
}
|