Skip to content

Commit 481e715

Browse files
ralyodioclaude
andcommitted
fix(arcade): give ncurses games a TERM so they launch
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>
1 parent beba7dc commit 481e715

3 files changed

Lines changed: 29 additions & 0 deletions

File tree

cmd/agentbbs/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,9 @@ func (a *app) teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
406406
go func() { <-s.Context().Done(); _ = a.st.EndSession(sessID) }()
407407

408408
ctx := plugin.Context{Store: a.st, Sandbox: a.sandbox, AssetsDir: a.assets, Host: a.host}
409+
if pty, _, ok := s.Pty(); ok {
410+
ctx.Term = pty.Term // ncurses arcade games need the client's TERM
411+
}
409412
if u.Kind != auth.Guest {
410413
ctx.DataDir = filepath.Join(a.dataDir, "users", u.Name)
411414
_ = os.MkdirAll(filepath.Join(ctx.DataDir, "wads"), 0o755)

internal/plugin/plugin.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ type Context struct {
2525
// Host is the BBS hostname (e.g. bbs.profullstack.com), for building
2626
// member homepage URLs (https://Host/~name) and similar links.
2727
Host string
28+
// Term is the client PTY's terminal type (e.g. xterm-256color). Needed by
29+
// sandboxed ncurses games (Space Invaders, Pac-Man, Tetris, Moon Patrol),
30+
// which call initscr() and fail with "Error opening terminal" if TERM is
31+
// unset — the systemd daemon environment has no TERM to inherit.
32+
Term string
2833
}
2934

3035
// Plugin is the only integration point between a feature and the hub.

plugins/arcade/arcade.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,32 @@ func (m *menu) workDir(sub string) string {
207207
return d
208208
}
209209

210+
// gameEnv is the minimal environment handed to a sandboxed game. It does NOT
211+
// inherit the daemon's environment (which carries operator secrets like
212+
// COINPAY_API_KEY) — a third-party game binary has no business seeing those.
213+
// TERM comes from the client PTY so ncurses games (Space Invaders, Pac-Man,
214+
// Tetris, Moon Patrol) can open the terminal; without it initscr() fails with
215+
// "Error opening terminal" and the game exits before drawing a frame.
216+
func (m *menu) gameEnv(work string) []string {
217+
term := m.ctx.Term
218+
if term == "" {
219+
term = "xterm-256color" // sane default if the client didn't request a PTY type
220+
}
221+
return []string{
222+
"TERM=" + term,
223+
"PATH=/usr/games:/usr/local/games:/usr/local/bin:/usr/bin:/bin",
224+
"HOME=" + work,
225+
"LANG=C.UTF-8", // unicode box-drawing for the ncurses games
226+
}
227+
}
228+
210229
// launchDoom suspends the TUI and bridges the session to a sandboxed
211230
// doom-ascii on a real PTY. Savegames land in the per-user work dir.
212231
func (m *menu) launchDoom(wad string) tea.Cmd {
213232
bin := doomBin(m.ctx)
214233
work := m.workDir(filepath.Join("doom", strings.TrimSuffix(filepath.Base(wad), filepath.Ext(wad))))
215234
cmd := m.ctx.Sandbox.Command(work, bin, "-iwad", wad)
235+
cmd.Env = m.gameEnv(work)
216236
return tea.Exec(newPtyExec(cmd, m.width, m.height), func(err error) tea.Msg {
217237
return gameDoneMsg{name: "DOOM", err: err}
218238
})
@@ -228,6 +248,7 @@ func (m *menu) launchExt(g extGame) tea.Cmd {
228248
}
229249
work := m.workDir(g.id)
230250
cmd := m.ctx.Sandbox.Command(work, bin, g.args...)
251+
cmd.Env = m.gameEnv(work)
231252
return tea.Exec(newPtyExec(cmd, m.width, m.height), func(err error) tea.Msg {
232253
return gameDoneMsg{name: g.label, err: err}
233254
})

0 commit comments

Comments
 (0)