mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
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>
102 lines
3.7 KiB
Go
102 lines
3.7 KiB
Go
// Package auth resolves SSH connections into AgentBBS identities (PRD §4.4).
|
|
package auth
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/ssh"
|
|
gossh "golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// Kind classifies an identity.
|
|
type Kind string
|
|
|
|
const (
|
|
Guest Kind = "guest"
|
|
Member Kind = "member"
|
|
Agent Kind = "agent"
|
|
)
|
|
|
|
// User is the resolved identity for one session.
|
|
type User struct {
|
|
Name string
|
|
Kind Kind
|
|
PubKeyFP string // SHA256 fingerprint, empty for guests without a key
|
|
StoreID int64 // 0 for guests
|
|
}
|
|
|
|
// GuestNames are usernames that always map to an anonymous guest hub session.
|
|
var GuestNames = map[string]bool{"bbs": true, "play": true, "guest": true}
|
|
|
|
// PodNames are usernames that route to a personal pod instead of the hub.
|
|
// Pod access requires an active paid membership (PRD pods addendum).
|
|
var PodNames = map[string]bool{"pod": true}
|
|
|
|
// JoinNames are usernames that trigger the onboarding flow: register the
|
|
// visitor's public key, print instructions, and disconnect.
|
|
var JoinNames = map[string]bool{"join": true, "signup": true, "register": true}
|
|
|
|
// DomainNames are usernames that route to the custom-domain self-service flow:
|
|
// list/add/remove the domains pointed at a member's homepage.
|
|
var DomainNames = map[string]bool{"domain": true, "domains": true}
|
|
|
|
// AdminNames are usernames that route to the privileged admin console (PRD §6).
|
|
// The route only opens for accounts whose name is in the operator allowlist
|
|
// (see IsAdmin); the name itself confers nothing.
|
|
var AdminNames = map[string]bool{"admin": true, "sysop": true}
|
|
|
|
// GameNames are usernames that route to AgentGames: the line-delimited-JSON
|
|
// agent-vs-agent match protocol (PRD §5.2). `play@` stays a guest hub alias.
|
|
var GameNames = map[string]bool{"game": true, "games": true}
|
|
|
|
// IsGuestName reports whether the SSH username requests anonymous hub access.
|
|
func IsGuestName(u string) bool { return GuestNames[strings.ToLower(u)] }
|
|
|
|
// IsPodName reports whether the SSH username requests the pod route.
|
|
func IsPodName(u string) bool { return PodNames[strings.ToLower(u)] }
|
|
|
|
// IsJoinName reports whether the SSH username requests onboarding.
|
|
func IsJoinName(u string) bool { return JoinNames[strings.ToLower(u)] }
|
|
|
|
// IsDomainName reports whether the SSH username requests the custom-domain flow.
|
|
func IsDomainName(u string) bool { return DomainNames[strings.ToLower(u)] }
|
|
|
|
// IsAdminName reports whether the SSH username requests the admin console.
|
|
func IsAdminName(u string) bool { return AdminNames[strings.ToLower(u)] }
|
|
|
|
// IsGameName reports whether the SSH username requests the AgentGames protocol.
|
|
func IsGameName(u string) bool { return GameNames[strings.ToLower(u)] }
|
|
|
|
// Admins returns the operator-configured admin allowlist: the lowercased,
|
|
// comma/space-separated account names in $AGENTBBS_ADMINS. Admin status can
|
|
// only be granted by the operator (via env), never self-assigned in-band.
|
|
func Admins() map[string]bool {
|
|
out := map[string]bool{}
|
|
for _, f := range strings.FieldsFunc(os.Getenv("AGENTBBS_ADMINS"), func(r rune) bool {
|
|
return r == ',' || r == ' ' || r == '\t' || r == '\n'
|
|
}) {
|
|
out[strings.ToLower(f)] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
// IsAdmin reports whether the account name is in the operator allowlist.
|
|
func IsAdmin(name string) bool { return Admins()[strings.ToLower(name)] }
|
|
|
|
// KindFor infers the identity kind from a (non-guest) username.
|
|
// Usernames prefixed "agent-" are automated clients (PRD §3).
|
|
func KindFor(username string) Kind {
|
|
if strings.HasPrefix(strings.ToLower(username), "agent-") {
|
|
return Agent
|
|
}
|
|
return Member
|
|
}
|
|
|
|
// Fingerprint returns the SHA256 fingerprint for a session public key, or "".
|
|
func Fingerprint(key ssh.PublicKey) string {
|
|
if key == nil {
|
|
return ""
|
|
}
|
|
return gossh.FingerprintSHA256(key)
|
|
}
|