From 3d727b9c7155a9c4c89e0136136b9b4f324a3e45 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 23 Oct 2026 06:18:12 -0700 Subject: [PATCH] Check agent CLI sign-in in server setup with a sign-in button --- README.md | 2 +- cmd/byre/check.go | 55 +++++++++++++++++++++++++++------- cmd/byre/tool.go | 35 +++++++++++++++++++++- internal/agents/kb/setup.md | 4 +-- mac/Sources/Byre/Server.swift | 8 ++++- mac/Sources/Byre/Startup.swift | 2 +- 6 files changed, 89 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c18f789..4761ab2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ A server and macOS app for running many coding agents (Claude Code, Codex and An macOS and a [Tailscale](https://tailscale.com) tailnet with MagicDNS and HTTPS certificates turned on. byre and the app each join it as their own node, so no Mac needs the Tailscale app. Remote access uses Tailscale identity: byre serves the tailnet's users, as its node identifies them. Local scripts use a bearer token to check server status and run commands on connected Macs (see [docs/operations.md](docs/operations.md#tailnet)). -The Mac that runs the server needs `git` and at least one agent CLI: Claude Code (`claude`), Codex (`codex`) or Antigravity (`agy`, for Gemini models). Browser tools also need Node.js (agents' Playwright tools run through `npx`) and Google Chrome. Codex, Antigravity, Node.js and Chrome install through Homebrew. The app's Server tab lists what's missing, each with a button that installs it and starts its sign-in. +The Mac that runs the server needs `git` and at least one agent CLI: Claude Code (`claude`), Codex (`codex`) or Antigravity (`agy`, for Gemini models). Browser tools also need Node.js (agents' Playwright tools run through `npx`) and Google Chrome. Codex, Antigravity, Node.js and Chrome install through Homebrew. The app's Server tab lists what's missing, and claude or codex installed but not signed in, each with a button that installs it if needed and starts its sign-in. ## Installing diff --git a/cmd/byre/check.go b/cmd/byre/check.go index cc3dc35..d498ccc 100644 --- a/cmd/byre/check.go +++ b/cmd/byre/check.go @@ -1,10 +1,12 @@ package main import ( + "context" "encoding/json" "fmt" "os" "os/exec" + "time" ) const ( @@ -14,13 +16,14 @@ const ( ) type check struct { - Name string `json:"name"` - Need string `json:"need"` - For string `json:"for"` - OK bool `json:"ok"` - Tool string `json:"tool"` - Fix string `json:"fix"` - Blocking bool `json:"blocking"` + Name string `json:"name"` + Need string `json:"need"` + For string `json:"for"` + OK bool `json:"ok"` + Tool string `json:"tool"` + Fix string `json:"fix"` + Blocking bool `json:"blocking"` + SignedOut bool `json:"signed_out"` } func checks() []check { @@ -28,8 +31,8 @@ func checks() []check { return []check{ brew, gitCheck(), - command("claude", "claude", needAgent, "Claude models", "claude"), - withBrew(brew, command("codex", "codex", needAgent, "GPT models", "codex")), + signedIn(command("claude", "claude", needAgent, "Claude models", "claude"), claudeSignedIn), + withBrew(brew, signedIn(command("codex", "codex", needAgent, "GPT models", "codex"), codexSignedIn)), withBrew(brew, command("agy", "agy", needAgent, "Gemini models", "agy")), withBrew(brew, command("npx", "npx", needOptional, "reviewers' Playwright browser tools", "node")), withBrew(brew, appCheck("Google Chrome", needOptional, "browser tools for agents and reviewers", "chrome")), @@ -54,8 +57,34 @@ func appCheck(name, need, purpose, tool string) check { return check{Name: name, Need: need, For: purpose, OK: err == nil, Tool: tool} } +func signedIn(c check, status func() bool) check { + if c.OK && !status() { + c.OK = false + c.SignedOut = true + } + return c +} + +func claudeSignedIn() bool { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + out, _ := exec.CommandContext(ctx, "claude", "auth", "status").Output() + status := struct { + LoggedIn bool `json:"loggedIn"` + }{} + return json.Unmarshal(out, &status) == nil && status.LoggedIn +} + +func codexSignedIn() bool { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "codex", "login", "status") + cmd.Env = append(os.Environ(), "CODEX_HOME="+codexHome()) + return cmd.Run() == nil +} + func withBrew(brew, c check) check { - if !c.OK && !brew.OK { + if !c.OK && !c.SignedOut && !brew.OK { c.Tool = "" c.Fix = "Install Homebrew first." } @@ -94,11 +123,15 @@ func requireChecks() { if markBlocking(cs) { return } - fmt.Println("byre can't run on this Mac yet. It needs git and at least one of claude, codex and agy:") + fmt.Println("byre can't run on this Mac yet. It needs git and at least one of claude, codex and agy, signed in:") for _, c := range cs { if !c.Blocking { continue } + if c.SignedOut { + fmt.Printf(" %s: not signed in: %s\n", c.Name, installCommand(c.Tool)) + continue + } if c.Tool == "" { fmt.Printf(" %s: %s %s\n", c.Name, c.Fix, installCommand("brew")) continue diff --git a/cmd/byre/tool.go b/cmd/byre/tool.go index 4c18e4e..8db402d 100644 --- a/cmd/byre/tool.go +++ b/cmd/byre/tool.go @@ -36,15 +36,28 @@ func installToolCmd(args []string) { run("xcode-select", "--install") fmt.Println("macOS is showing its own dialog to install the Command Line Tools: click Install there.") case "claude": + path, err := exec.LookPath("claude") + if err == nil { + signIn(path, "auth", "login") + } run("/bin/sh", "-c", "curl -fsSL https://claude.ai/install.sh | bash") addToProfile(".zshrc", `export PATH="$HOME/.local/bin:$PATH"`) - signIn(filepath.Join(homeDir(), ".local", "bin", "claude")) + signIn(filepath.Join(homeDir(), ".local", "bin", "claude"), "auth", "login") default: t, ok := brewTools[args[0]] if !ok { fmt.Fprintln(os.Stderr, "unknown tool "+args[0]+": NAME is "+toolNames) os.Exit(2) } + if args[0] == "codex" { + useCodexHome() + } + if t.signIn != nil { + path, err := exec.LookPath(t.signIn[0]) + if err == nil { + signIn(path, t.signIn[1:]...) + } + } brew, err := exec.LookPath("brew") if err != nil { fmt.Println("Homebrew isn't installed. Install it first:", installCommand("brew")) @@ -65,6 +78,26 @@ func installCommand(name string) string { return "'" + strings.ReplaceAll(exe, "'", `'\''`) + "' install-tool " + name } +func codexHome() string { + configDir, err := os.UserConfigDir() + if err != nil { + fatal("user config dir", "err", err) + } + return filepath.Join(configDir, "byre", "codex") +} + +func useCodexHome() { + home := codexHome() + err := os.MkdirAll(home, 0o700) + if err != nil { + fatal("create codex home", "path", home, "err", err) + } + err = os.Setenv("CODEX_HOME", home) + if err != nil { + fatal("set CODEX_HOME", "err", err) + } +} + func homeDir() string { home, err := os.UserHomeDir() if err != nil { diff --git a/internal/agents/kb/setup.md b/internal/agents/kb/setup.md index a71ab73..0fffbfd 100644 --- a/internal/agents/kb/setup.md +++ b/internal/agents/kb/setup.md @@ -14,10 +14,10 @@ Agents run on the Mac byre runs on, the one your own Bash runs on. Check its too "$SHELL" -l -i -c '/Applications/Byre.app/Contents/Helpers/byre check' ``` -It prints one JSON line: `ready` (false while something blocking is missing) and `checks`, each with `name`, `need` (`required`; `agent`, where one of claude, codex and agy is enough; or `optional`), `for` (what it is used for), `ok`, `tool` (what installs it: `byre install-tool `; empty while Homebrew, which it needs, is missing), `fix` (what to do instead) and `blocking`. +It prints one JSON line: `ready` (false while something blocking is missing) and `checks`, each with `name`, `need` (`required`; `agent`, where one of claude, codex and agy is enough; or `optional`), `for` (what it is used for), `ok`, `tool` (what installs it: `byre install-tool `; empty while Homebrew, which it needs, is missing), `fix` (what to do instead), `blocking` and `signed_out` (claude or codex is installed but not signed in, which also makes `ok` false). - `byre install-tool ` installs one tool: `brew` (Homebrew, and its `shellenv` line in `~/.zprofile`), `git` (`xcode-select --install`, which opens a macOS dialog for the human to click Install), `claude` (its installer, and `~/.local/bin` on `PATH` in `~/.zshrc`), and `codex`, `agy`, `node` and `chrome` through Homebrew. Homebrew's installer asks for the human's password, and `claude`, `codex` and `agy` end by starting the CLI for the human to sign in, so these need the human at a terminal on that Mac: type the command into the task's terminal without the newline and ask them to press Enter, or point them at the app's Server tab there, which lists each missing tool with a Run in Terminal button. `node` and `chrome` you can run yourself through `"$SHELL" -l -c ''`. -- Sign-in, when a CLI is installed but not signed in: `claude`, `codex login` or `agy`, each run once by the human. `claude auth status` (JSON, `loggedIn`) and `codex login status` show whether one is signed in; agy has no status command, so ask. +- Sign-in, when a CLI is installed but not signed in: `byre install-tool ` skips the install and just starts the sign-in, run once by the human like the installs. byre's Codex has its own home, so Codex is signed in there, not in `~/.codex`; `install-tool codex` handles that. agy has no status command, so ask whether it is signed in. - The service's `PATH` is fixed when it is installed, and your own shell has the same one. It comes from an interactive login shell (`-l -i`), since claude's installer has the human add `~/.local/bin` to `.zshrc`. When a tool is found through that shell but not by `/Applications/Byre.app/Contents/Helpers/byre check` run directly, reinstall the service from it so it picks the new `PATH` up: `"$SHELL" -l -i -c '/Applications/Byre.app/Contents/Helpers/byre install'`. That restarts byre and cuts your turn off; you resume with a restart message, so say before running it that you will carry on afterwards. When you resume, check with `byre check` before reinstalling again: the restart usually took. - `byre install` and the app's Server tab (Turn off, Turn on) are the only ways to restart byre's service. Never run `launchctl` on it yourself, and never tell the human to. - Google Chrome, for agents' browser tools, asks "downloaded from the Internet" on its first launch, and can't be driven until the human answers Open. diff --git a/mac/Sources/Byre/Server.swift b/mac/Sources/Byre/Server.swift index aa316de..6e6c0aa 100644 --- a/mac/Sources/Byre/Server.swift +++ b/mac/Sources/Byre/Server.swift @@ -36,6 +36,12 @@ struct Check: Decodable { let tool: String let fix: String let blocking: Bool + let signedOut: Bool + + enum CodingKeys: String, CodingKey { + case name, need, `for`, ok, tool, fix, blocking + case signedOut = "signed_out" + } } struct Checks: Decodable { @@ -75,7 +81,7 @@ struct ChecksView: View { if check.ok { return Text("Found").foregroundStyle(Palette.ok) } - return Text("Missing").foregroundStyle(check.blocking ? Palette.danger : Palette.muted) + return Text(check.signedOut ? "Not signed in" : "Missing").foregroundStyle(check.blocking ? Palette.danger : Palette.muted) } private func need(_ check: Check) -> String { diff --git a/mac/Sources/Byre/Startup.swift b/mac/Sources/Byre/Startup.swift index 81c157c..e4f1594 100644 --- a/mac/Sources/Byre/Startup.swift +++ b/mac/Sources/Byre/Startup.swift @@ -451,7 +451,7 @@ struct ConnectView: View { } #if os(macOS) if !form.missing.isEmpty { - Text("byre can't run on this Mac yet. Install what's missing, then click Run byre on this Mac again.") + Text("byre can't run on this Mac yet. Install what's missing and sign in, then click Run byre on this Mac again.") .fixedSize(horizontal: false, vertical: true) .padding(.top, 6) ChecksView(checks: form.missing)