One phase. Fix all auth bugs. Clean seam for future providers.
Beast Computer's Claude OAuth flow has 5 silent break points in the golden path. Any one of them leaves the user staring at a spinner forever with no actionable error.
| Break Point | Where | What Happens | Severity |
|---|---|---|---|
| In-memory auth state | web_auth_flow.go:44 | Server restart loses all auth progress. User must start over. | Critical |
| ANSI URL corruption | web_auth_flow.go:141 | OSC hyperlinks and split PTY reads corrupt OAuth URL extraction. User sees "Getting your login link..." forever. | Critical |
| Settings merge bail-out | template.go:346 | mergeSettingsHooks() returns if any hooks key exists. Beast Stop hook never installed. Chat responses never reach web UI. | Critical |
| Success inferred from PTY | web_auth_flow.go:186 | Auth success inferred from cmd.Wait() exit code + checkClaudeAuth(). If check fails (systemd noise, JSON parse), state=failed even though user IS logged in. | High |
| No auth gate on Start() | manager.go:284 | Claude launches but hits 5+ onboarding prompts. User sees "Online" but messages go to prompts, not Claude. | High |
Codex read all project MD files (IDEA, ARCHITECTURE, EXECUTION, COMPUTER) plus the full auth codebase, then reviewed the original 5-phase design against 7 adversarial questions.
"The diagnosis is right, but the proposed architecture is bloated. Fix the current Claude flow, add a thin provider seam, and stop before building a second supervisor/state machine."
| # | Finding | Resolution |
|---|---|---|
| 1 | 13 states is too many — mixes auth, setup, and runtime. starting_computer and running already belong to Manager. | Fixed |
| 2 | OnboardingRules catalog is a trap — Claude prompts aren't a stable API. Complete() will rot. | Fixed |
| 3 | Prepare/Complete don't belong in AuthProvider — hook/config merging belongs in template.go. | Fixed |
| 4 | Interface too wide — Status/Verify duplicate. Start with 5 methods max. | Fixed |
| 5 | Reconcile-on-every-poll will hurt — CLI exec on every 8s HTMX poll is wasteful. | Fixed |
| 6 | Missing edge cases: double-tab, cancel/submit race, stale PTY, duplicate code, CLI upgrade, provider hang, API key leak, corrupted JSON, disk full, DB lock. | Fixed |
| 7 | Phase granularity wrong — don't bundle TerminalDriver onboarding automation with core auth fix. | Fixed |
Round 2: Codex designed "Durable Provider Auth Gate" — one shipping phase that fixes all bugs with a clean interface seam. No onboarding automation, no runtime state mixing, no per-poll CLI exec.
type AuthProvider interface {
Metadata() AuthProviderMetadata
Status(ctx context.Context, c *Computer) (*AuthStatus, error)
Begin(ctx context.Context, c *Computer, s *AuthSession) (*AuthChallenge, error)
Submit(ctx context.Context, c *Computer, s *AuthSession, input AuthInput) (*AuthStatus, error)
Cancel(ctx context.Context, c *Computer, s *AuthSession) error
}
type AuthProviderMetadata struct {
ID string `json:"id"` // "claude"
DisplayName string `json:"display_name"` // "Claude"
Method string `json:"method"` // "oauth_code"
}
No Prepare(). No Complete(). No Probe(). No Verify(). Workspace config stays in template.go. Auth does auth.
type AuthState string
const (
AuthUnknown AuthState = "unknown"
AuthUnauthenticated AuthState = "unauthenticated"
AuthChallengePending AuthState = "challenge_pending"
AuthVerifying AuthState = "verifying"
AuthAuthenticated AuthState = "authenticated"
AuthBlocked AuthState = "blocked"
AuthFailed AuthState = "failed"
)
type AuthStatus struct {
State AuthState `json:"state"`
Provider string `json:"provider"`
Detail string `json:"detail,omitempty"`
CheckedAt int64 `json:"checked_at"`
ExpiresAt int64 `json:"expires_at,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorText string `json:"error_text,omitempty"`
}
type AuthChallenge struct {
URL string `json:"url,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
SubmitHint string `json:"submit_hint,omitempty"` // "paste_code"
}
type AuthInput struct {
Code string `json:"code,omitempty"`
}
Runtime states (starting, running, stopped) stay in Computer.State / Manager. Auth and runtime never mix.
CREATE TABLE IF NOT EXISTS auth_sessions (
id TEXT PRIMARY KEY,
computer TEXT NOT NULL,
provider TEXT NOT NULL,
state TEXT NOT NULL,
challenge_url TEXT NOT NULL DEFAULT '',
challenge_expires_at INTEGER,
status_checked_at INTEGER,
status_expires_at INTEGER,
lease_owner TEXT NOT NULL DEFAULT '',
lease_until INTEGER,
attempt INTEGER NOT NULL DEFAULT 1,
submit_hash TEXT NOT NULL DEFAULT '',
error_code TEXT NOT NULL DEFAULT '',
error_text TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
completed_at INTEGER,
cancelled_at INTEGER
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_auth_active
ON auth_sessions(computer, provider)
WHERE state IN ('challenge_pending', 'verifying');
CREATE INDEX IF NOT EXISTS idx_auth_lookup
ON auth_sessions(computer, provider, updated_at);
lease_owner + lease_until prevent concurrent Begin() from two server instanceshash(code+sessionID) rejects duplicate code submissionEnsureReady() does CLI exec when TTL is expiredManager.Start() calls AuthService.EnsureReady(ctx, c) before launching tmux. This is the sole path that may run claude auth status.
func (s *AuthService) EnsureReady(ctx context.Context, c *Computer) error {
// 1. Read latest auth_session from DB
session := s.db.LatestAuthStatus(c.Name, providerID)
// 2. If authenticated and TTL valid → return nil (cached)
if session.State == AuthAuthenticated && session.StatusExpiresAt > now {
return nil
}
// 3. Stale or unknown → run provider.Status() with short timeout
status, err := s.provider.Status(ctx, c)
if err != nil {
return fmt.Errorf("auth check failed: %w", err)
}
// 4. Update DB with fresh status + new TTL
s.db.UpdateAuthSession(session.ID, func(r *AuthSessionRecord) {
r.State = status.State
r.StatusCheckedAt = now
r.StatusExpiresAt = now + TTL
})
// 5. If not authenticated → return error (blocks Start)
if status.State != AuthAuthenticated {
return fmt.Errorf("auth required: state=%s", status.State)
}
return nil
}
Also called by: supervisor recovery, chat handler (before first message), ResumeComputer().
HTMX polling reads auth_sessions table from controldb. Never shells out. All CLI exec happens in EnsureReady() with TTL caching.
| Endpoint | Action | State Shown |
|---|---|---|
GET /ui/auth | Read DB row | Render based on auth_sessions.state |
POST /auth/start | Create/reuse session | Partial unique index prevents double-tab |
POST /auth/code | Submit via provider | Hash dedup rejects duplicate submit |
POST /auth/cancel | Cancel active flow | Back to unauthenticated |
| DB State | UI Renders |
|---|---|
unknown / unauthenticated | "Connect your AI" button |
challenge_pending | Login URL + code input form |
verifying | "Connecting..." spinner |
authenticated | Hide onboarding, trigger start |
failed | Error message + "Try again" button |
blocked | Specific guidance (CLI missing, env issue) |
| File | Action | What Changes |
|---|---|---|
auth_provider.go | New | AuthProvider interface (5 methods), AuthState, AuthStatus, AuthChallenge, AuthInput types |
auth_service.go | New | AuthService: EnsureReady, BeginAuth, SubmitAuth, CancelAuth, CachedStatus. Owns TTL, DB writes, leases, dedup, events |
auth_claude.go | New | ClaudeAuthProvider: 5 methods. PTY via terminal_driver. ANSI-stripped URL extraction. claude auth status --json for verification |
terminal_driver.go | New | Small PTY helper: bounded buffer, ANSI strip, URL extract, write-line with mutex, context timeout, kill process group |
controldb/auth.go | New | AuthSessionRecord, CurrentAuthSession, InsertAuthSession, UpdateAuthSession, LatestAuthStatus, ExpireStale |
controldb/migrate.go | Modify | Add migration 5: auth_sessions table + indexes |
manager.go | Modify | Add auth *AuthService. Gate Start() and ResumeComputer() on EnsureReady() |
web_auth_flow.go | Modify | Replace global authFlows map. In-process PTY handles keyed by session ID. All visible state from DB |
template.go | Fix | Deep-merge Beast Stop hook (insert/dedup), corrupted JSON recovery (backup + rewrite), atomic write (temp+fsync+rename) |
web_api.go | Modify | handleUIStart returns auth fragment if not ready (not "Could not wake up") |
web_dashboard.go | Modify | Auth HTMX renders from DB state. Provider-agnostic labels |
| Edge Case | Solution |
|---|---|
| Multiple tabs click "Connect" | Partial unique index on (computer, provider) WHERE state IN active. Second tab gets existing session. |
| Cancel races with Submit | DB state checked before PTY write. If cancelled, Submit returns error. |
| Duplicate code submission | submit_hash column. hash(code+sessionID) rejects duplicate. |
| Server restart mid-auth | Durable state in controldb. PTY gone, state persists. EnsureReady() runs fresh Status(). |
| Claude CLI upgraded during flow | Begin() re-probes CLI flags each time (no cached probe). |
| Provider command hangs | Context timeout on all provider calls. Kill process group on timeout. |
| Corrupted settings.json | template.go backup as .beast-bad-{ts}, install clean settings, atomic write. |
| Disk full during auth | Atomic write detects fsync failure. State stays in DB, retryable. |
| Computer removed during auth | AuthService checks computer exists before each state transition. |
| API keys in transcripts | terminal_driver never logs auth codes. Auth input redacted in events. |
// In manager.go initialization — one line:
authProviders["codex"] = NewCodexAuthProvider(cfg.Agent.CodexBin, m.runtime)
// CodexAuthProvider implements same 5 methods:
// Metadata() → {ID:"codex", Method:"api_key"}
// Status() → check OPENAI_API_KEY env/file
// Begin() → return challenge with SubmitHint:"paste_api_key"
// Submit() → validate key, write to env
// Cancel() → cleanup
No UI rewrite. No manager rewrite. No new DB table. Same auth_sessions, same state machine. Codex validates the seam is clean.
One phase ships together. Fixes all 5 break points:
auth_sessions in controldbclaude auth status --jsonEnsureReady() blocks Manager.Start()Clean 5-method interface seam. Codex validates it later — it's the test, not the reason to over-engineer now.
Research artifact for Beast Computer auth architecture. Produced through 3 rounds of adversarial review with Codex gpt-5.5. All project MD files (IDEA, ARCHITECTURE, EXECUTION, COMPUTER) read and incorporated. Prior version (5-phase design with 13-state machine) was rejected after review.