server and settings setup
This commit is contained in:
+12
-10
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/ai/byre/internal/tailscale"
|
||||
)
|
||||
@@ -21,6 +22,7 @@ type check struct {
|
||||
Need string `json:"need"`
|
||||
For string `json:"for"`
|
||||
OK bool `json:"ok"`
|
||||
Command string `json:"command"`
|
||||
Fix string `json:"fix"`
|
||||
Blocking bool `json:"blocking"`
|
||||
}
|
||||
@@ -29,16 +31,16 @@ func checks() []check {
|
||||
return []check{
|
||||
gitCheck(),
|
||||
tailscaleCheck(),
|
||||
command("claude", needAgent, "Claude models", "curl -fsSL https://claude.ai/install.sh | bash, then run claude once to sign in"),
|
||||
command("codex", needAgent, "GPT models", "brew install --cask codex, then run codex once to sign in"),
|
||||
command("agy", needAgent, "Gemini models", "brew install --cask antigravity-cli, then run agy once to sign in"),
|
||||
command("npx", needOptional, "reviewers' Playwright browser tools", "brew install node"),
|
||||
command("claude", needAgent, "Claude models", "curl -fsSL https://claude.ai/install.sh | bash", "Then run claude once to sign in."),
|
||||
command("codex", needAgent, "GPT models", "brew install --cask codex", "Then run codex once to sign in."),
|
||||
command("agy", needAgent, "Gemini models", "brew install --cask antigravity-cli", "Then run agy once to sign in."),
|
||||
command("npx", needOptional, "reviewers' Playwright browser tools", "brew install node", ""),
|
||||
appCheck("Google Chrome", needOptional, "browser tools for agents and reviewers", "brew install --cask google-chrome"),
|
||||
}
|
||||
}
|
||||
|
||||
func gitCheck() check {
|
||||
c := check{Name: "git", Need: needRequired, For: "worktrees, snapshots and merges", Fix: "xcode-select --install"}
|
||||
c := check{Name: "git", Need: needRequired, For: "worktrees, snapshots and merges", Command: "xcode-select --install"}
|
||||
path, err := exec.LookPath("git")
|
||||
// /usr/bin/git is a stub that offers to install the Command Line Tools when they're missing.
|
||||
c.OK = err == nil && (path != "/usr/bin/git" || exec.Command("xcode-select", "-p").Run() == nil)
|
||||
@@ -50,14 +52,14 @@ func tailscaleCheck() check {
|
||||
return check{Name: "Tailscale", Need: needRequired, For: "the server's HTTPS address and identity", OK: problem == "", Fix: problem}
|
||||
}
|
||||
|
||||
func command(name, need, purpose, fix string) check {
|
||||
func command(name, need, purpose, install, fix string) check {
|
||||
_, err := exec.LookPath(name)
|
||||
return check{Name: name, Need: need, For: purpose, OK: err == nil, Fix: fix}
|
||||
return check{Name: name, Need: need, For: purpose, OK: err == nil, Command: install, Fix: fix}
|
||||
}
|
||||
|
||||
func appCheck(name, need, purpose, fix string) check {
|
||||
func appCheck(name, need, purpose, install string) check {
|
||||
_, err := os.Stat("/Applications/" + name + ".app")
|
||||
return check{Name: name, Need: need, For: purpose, OK: err == nil, Fix: fix}
|
||||
return check{Name: name, Need: need, For: purpose, OK: err == nil, Command: install}
|
||||
}
|
||||
|
||||
func markBlocking(cs []check) bool {
|
||||
@@ -94,7 +96,7 @@ func requireChecks() {
|
||||
fmt.Println("byre can't run on this Mac yet. It needs git, Tailscale, and at least one of claude, codex and agy:")
|
||||
for _, c := range cs {
|
||||
if c.Blocking {
|
||||
fmt.Printf(" %s: %s\n", c.Name, c.Fix)
|
||||
fmt.Printf(" %s: %s\n", c.Name, strings.TrimSpace(c.Command+" "+c.Fix))
|
||||
}
|
||||
}
|
||||
os.Exit(1)
|
||||
|
||||
+2
-2
@@ -117,7 +117,7 @@ A team task has several agents working at once in its one worktree, branch and p
|
||||
|
||||
The listen address is `byre serve --listen`, whose default is `defaultListen` in `cmd/byre/serve.go` (`go run ./cmd/byre serve --help` prints it); keep it on loopback, since identity relies on requests arriving from `tailscale serve`.
|
||||
|
||||
`byre check` (`cmd/byre/check.go`) prints, as one line of JSON, what the server needs from the Mac it runs on, found through the `PATH` it was run with: each check's name, need (`required`: `git`, counted only when `/usr/bin/git`'s Command Line Tools are installed (`xcode-select -p`), and Tailscale, which must be installed at `/Applications/Tailscale.app`, signed in (`BackendState` `Running` in `tailscale status --json`) and have HTTPS certificates (non-empty `CertDomains`); `agent`: `claude`, `codex` and `agy`, at least one of which is required; `optional`: `npx` and Google Chrome in `/Applications`), what it is for, whether it was found, how to install it, and whether its absence blocks the server, with `ready` true when nothing blocks. `byre install` (`cmd/byre/install.go`) runs the same checks first and, when one blocks, prints each blocking one with how to install it and exits 1 without changing anything. Otherwise it sets the service up for the binary it is run as, with `byre-hands` beside it: it writes `~/Library/LaunchAgents/run.fc.byre.plist` running `<that binary> serve`, with a `PATH` of that binary's directory (so `byre-hands` beside it is found) followed by the `PATH` of the shell it was run from (which must find `claude`, `codex`, `agy`, `curl`, `git` and `npx`, `curl` for Antigravity's hooks and `npx` for the Playwright MCP server, which also needs Google Chrome) and logs to `~/Library/Logs/byre.log`; runs `tailscale serve --bg --https=443 http://<default listen address>` through the Tailscale app's CLI (`tailscale.ServeRoot`, see "Previews"), printing `byre is at https://<machine>.<tailnet>.ts.net`; and bootstraps the service. When the service is already loaded, it hands `launchctl bootout` and `bootstrap` to a detached `sh` in its own session and exits, so the new plist is read: `bootout` kills every process in the service's process groups, which includes `byre install` itself when an agent byre runs is the one running it. The service always runs from the app, `/Applications/Byre.app/Contents/Helpers/byre install`, which the app's Server tab and connection screen run for you (see "UI").
|
||||
`byre check` (`cmd/byre/check.go`) prints, as one line of JSON, what the server needs from the Mac it runs on, found through the `PATH` it was run with: each check's name, need (`required`: `git`, counted only when `/usr/bin/git`'s Command Line Tools are installed (`xcode-select -p`), and Tailscale, which must be installed at `/Applications/Tailscale.app`, signed in (`BackendState` `Running` in `tailscale status --json`) and have HTTPS certificates (non-empty `CertDomains`); `agent`: `claude`, `codex` and `agy`, at least one of which is required; `optional`: `npx` and Google Chrome in `/Applications`), what it is for, whether it was found, the shell command that installs it (`command`, empty for Tailscale) and any further step (`fix`, such as signing in, or what is wrong with Tailscale), and whether its absence blocks the server, with `ready` true when nothing blocks. `byre install` (`cmd/byre/install.go`) runs the same checks first and, when one blocks, prints each blocking one with how to install it and exits 1 without changing anything. Otherwise it sets the service up for the binary it is run as, with `byre-hands` beside it: it writes `~/Library/LaunchAgents/run.fc.byre.plist` running `<that binary> serve`, with a `PATH` of that binary's directory (so `byre-hands` beside it is found) followed by the `PATH` of the shell it was run from (which must find `claude`, `codex`, `agy`, `curl`, `git` and `npx`, `curl` for Antigravity's hooks and `npx` for the Playwright MCP server, which also needs Google Chrome) and logs to `~/Library/Logs/byre.log`; runs `tailscale serve --bg --https=443 http://<default listen address>` through the Tailscale app's CLI (`tailscale.ServeRoot`, see "Previews"), printing `byre is at https://<machine>.<tailnet>.ts.net`; and bootstraps the service. When the service is already loaded, it hands `launchctl bootout` and `bootstrap` to a detached `sh` in its own session and exits, so the new plist is read: `bootout` kills every process in the service's process groups, which includes `byre install` itself when an agent byre runs is the one running it. The service always runs from the app, `/Applications/Byre.app/Contents/Helpers/byre install`, which the app's Server tab and connection screen run for you (see "UI").
|
||||
|
||||
byre is served at the root of the machine's `ts.net` name on 443 (`https://<machine>.<tailnet>.ts.net`), not under a path: the API uses absolute paths, and the macOS app takes an origin with no path. `tailscale serve status` shows the URL and what else the machine serves. `launchctl print gui/$(id -u)/run.fc.byre` shows the service and the program it runs.
|
||||
|
||||
@@ -281,7 +281,7 @@ Each user has one long-running lead agent, shown in the left pane. It is not tie
|
||||
Task agents get notes too. An implementer session's instructions end with `taskNotesGuidance` and `notesText` for the task's creator, limited to the every-project notes and the task's project's (`startLive` in `internal/agents/manager.go`), fixed when the agent process starts like the lead's. Reviewers get none. Implementers have a `suggest_note` tool (`text`, `evidence`) that queues a lead signal for the task's creator naming the task, the suggested text, the evidence and the project id, and refuses when the creator has no live lead session. The lead's guidance tells it to save a suggestion with `remember`, choosing its project, only when it holds and isn't already covered, and to tell the human nothing either way. A suggestion leaves no record of its own beyond the signal; a saved one shows as the lead's `lead.note_saved`.
|
||||
- **Restarts.** On shutdown, byre marks each running lead session idle with "byre stopped mid-turn" and holds a restart notice for it, so a lead is never left `running` for a build without lead support to find. After a crash, startup resumes a running lead like a task session. Startup then delivers any held input.
|
||||
|
||||
The UI is two panes filling the window, with no navbar. The left has six tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Notes, the lead's notes (see "Notes" under the lead), Workflows, Status, Server and Settings, and at the end of its tab strip the connection dot and the user's name. Workflows (`mac/Sources/Byre/Workflows.swift`) lists `GET /api/workflows` by name with each description, refetching on `workflow.updated` and `workflow.deleted`; a workflow's run button doesn't start anything itself, since a run needs a project and a brief: it appends `Run the "<name>" workflow: ` to the lead's composer draft, selects the Lead tab and focuses the composer, for the human to finish and send. Server is about the byre service on the Mac the app runs on, whatever server the app is connected to (`mac/Sources/Byre/Server.swift`): it is On when launchd has the service loaded (`launchctl print gui/<uid>/run.fc.byre` succeeds), showing the address from `tailscale serve status --json` and Turn off (`launchctl bootout`), and Off otherwise, showing Turn on, which runs the bundle's `byre install` as the connection screen's Run byre on this Mac does. Below that, Tools on this Mac lists every check from the bundle's `byre check` (see "Running as a service"), each Found or Missing, with how to install a missing one; Missing is in the danger colour only when it blocks the server. Run byre on this Mac runs `byre check` first, and when something blocks it lists the missing checks in the same form instead of installing. Both Turn on and Run byre on this Mac use the running service's address instead of reinstalling when it is already on, since a reinstall restarts it and the old service could answer the first status check. The app runs both helpers through the user's login shell, so they see the `PATH` the service is given. It reads the state and checks again each time it shows and when the app becomes active. While the service is on, Agents' permissions has Check permissions, which calls `POST /api/admin/permissions` on the service at that address (see "Permissions" under "Screen and app control") and lists each grant as Granted, Missing (with Open System Settings, which opens the grant's pane) or Not checked. Settings holds the user's and this client's preferences; its This Mac's permissions has the same Check permissions for the app's own grants, the ones `client_hands` uses, running the bundle's `byre-hands check` from a copy in the temporary directory (`checkLocalPermissions` in `Server.swift`). Which left tab is selected is kept only in memory (`Overlays`); the Lead view stays mounted while another tab shows, and Notes, Workflows, Status, Server or Settings mounts each time its tab is selected. The right shows the focused task, or project with no task: a top row holding the task picker, centred, and the task's preview links at its right end (see "Previews"), then the tab strip, then the selected tab's view with its own header. The tabs are Transcript, Board, Changes, Files, Terminal, Workflow and Project, always in that order. Board shows only on a team task (see "Teams") and Workflow only on a task with a `workflow_run` (see "Workflows"); the selected tab always shows. Tabs that don't fit in the strip move, from the end, into a **More ▾** menu (`MoreButton` in `mac/Sources/Byre/Panes.swift`); the selected tab never moves there. Questions and reviews have no tabs: both live in the transcript (see "Inbox" and "Reviews"), and the transcript's attention buttons, at its top right, count what needs the user there (see "Attention" under "Inbox"). The **Go** menu holds the shortcuts, each also on its Ctrl key outside the terminal. ⌘K opens the task switcher. ⌘P selects the Project tab, keeping the right pane's task. ⌘L selects the Lead tab and puts the cursor in the lead's composer. ⌘S opens the right pane's task's Merge confirmation, from any tab, when the Merge button would be enabled. ⌘/ opens a dialog listing every keyboard shortcut; the list is `shortcuts` in `mac/Sources/Byre/Switcher.swift`, kept in step with the menu by hand. The shortcuts do nothing while a dialog is open.
|
||||
The UI is two panes filling the window, with no navbar. The left has six tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Notes, the lead's notes (see "Notes" under the lead), Workflows, Status, Server and Settings, and at the end of its tab strip the connection dot and the user's name. Workflows (`mac/Sources/Byre/Workflows.swift`) lists `GET /api/workflows` by name with each description, refetching on `workflow.updated` and `workflow.deleted`; a workflow's run button doesn't start anything itself, since a run needs a project and a brief: it appends `Run the "<name>" workflow: ` to the lead's composer draft, selects the Lead tab and focuses the composer, for the human to finish and send. Server is about the byre service on the Mac the app runs on, whatever server the app is connected to (`mac/Sources/Byre/Server.swift`): it is On when launchd has the service loaded (`launchctl print gui/<uid>/run.fc.byre` succeeds), showing the address from `tailscale serve status --json` and Turn off (`launchctl bootout`), and Off otherwise, showing Turn on, which runs the bundle's `byre install` as the connection screen's Run byre on this Mac does. Below that, Tools on this Mac lists every check from the bundle's `byre check` (see "Running as a service"), each Found or Missing, with how to install a missing one: its command in a box with Copy and Run in Terminal, which writes the command to a `.command` file in the temporary directory, run by the user's login shell, and opens it in Terminal, so prompts like sudo's work and the list updates when the app becomes active again; Missing is in the danger colour only when it blocks the server. Run byre on this Mac runs `byre check` first, and when something blocks it lists the missing checks in the same form instead of installing. Both Turn on and Run byre on this Mac use the running service's address instead of reinstalling when it is already on, since a reinstall restarts it and the old service could answer the first status check. The app runs both helpers through the user's login shell, so they see the `PATH` the service is given. It reads the state and checks again each time it shows and when the app becomes active. While the service is on, Agents' permissions has Check permissions, which calls `POST /api/admin/permissions` on the service at that address (see "Permissions" under "Screen and app control") and lists each grant as Granted, Missing (with Open System Settings, which opens the grant's pane) or Not checked. Settings holds the user's and this client's preferences; its This Mac's permissions has the same Check permissions for the app's own grants, the ones `client_hands` uses, running the bundle's `byre-hands check` from a copy in the temporary directory (`checkLocalPermissions` in `Server.swift`). Which left tab is selected is kept only in memory (`Overlays`); the Lead view stays mounted while another tab shows, and Notes, Workflows, Status, Server or Settings mounts each time its tab is selected. The right shows the focused task, or project with no task: a top row holding the task picker, centred, and the task's preview links at its right end (see "Previews"), then the tab strip, then the selected tab's view with its own header. The tabs are Transcript, Board, Changes, Files, Terminal, Workflow and Project, always in that order. Board shows only on a team task (see "Teams") and Workflow only on a task with a `workflow_run` (see "Workflows"); the selected tab always shows. Tabs that don't fit in the strip move, from the end, into a **More ▾** menu (`MoreButton` in `mac/Sources/Byre/Panes.swift`); the selected tab never moves there. Questions and reviews have no tabs: both live in the transcript (see "Inbox" and "Reviews"), and the transcript's attention buttons, at its top right, count what needs the user there (see "Attention" under "Inbox"). The **Go** menu holds the shortcuts, each also on its Ctrl key outside the terminal. ⌘K opens the task switcher. ⌘P selects the Project tab, keeping the right pane's task. ⌘L selects the Lead tab and puts the cursor in the lead's composer. ⌘S opens the right pane's task's Merge confirmation, from any tab, when the Merge button would be enabled. ⌘/ opens a dialog listing every keyboard shortcut; the list is `shortcuts` in `mac/Sources/Byre/Switcher.swift`, kept in step with the menu by hand. The shortcuts do nothing while a dialog is open.
|
||||
|
||||
## TODO
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ struct Check: Decodable {
|
||||
let need: String
|
||||
let `for`: String
|
||||
let ok: Bool
|
||||
let command: String
|
||||
let fix: String
|
||||
let blocking: Bool
|
||||
}
|
||||
@@ -39,9 +40,14 @@ struct ChecksView: View {
|
||||
}
|
||||
Text("\(need(check)), for \(check.for)").foregroundStyle(Palette.muted)
|
||||
if !check.ok {
|
||||
Text(check.fix)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if !check.command.isEmpty {
|
||||
CommandBox(command: check.command)
|
||||
}
|
||||
if !check.fix.isEmpty {
|
||||
Text(check.fix)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +73,66 @@ struct ChecksView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct CommandBox: View {
|
||||
let command: String
|
||||
@ViewState private var copied = false
|
||||
@ViewState private var failure: String?
|
||||
|
||||
var body: some View {
|
||||
box
|
||||
if let failure {
|
||||
Text(failure)
|
||||
.foregroundStyle(Palette.danger)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
private var box: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(verbatim: command)
|
||||
.font(Fonts.font(12))
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Button(copied ? "Copied" : "Copy", action: copy)
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Copy the command to the clipboard")
|
||||
Button("Run in Terminal", action: run)
|
||||
.buttonStyle(SmallLinkButtonStyle())
|
||||
.help("Open Terminal and run the command")
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border))
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private func copy() {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(command, forType: .string)
|
||||
copied = true
|
||||
Task {
|
||||
try? await Task.sleep(for: .milliseconds(1500))
|
||||
copied = false
|
||||
}
|
||||
}
|
||||
|
||||
private func run() {
|
||||
let shell = unsafe String(cString: getpwuid(getuid()).pointee.pw_shell)
|
||||
let file = FileManager.default.temporaryDirectory.appending(path: "byre-install-\(UUID().uuidString).command")
|
||||
do {
|
||||
try "#!\(shell) -l\n\(command)\n".write(to: file, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: file.path)
|
||||
failure = nil
|
||||
} catch {
|
||||
failure = errorText(error)
|
||||
return
|
||||
}
|
||||
NSWorkspace.shared.open([file], withApplicationAt: URL(filePath: "/System/Applications/Utilities/Terminal.app"), configuration: NSWorkspace.OpenConfiguration())
|
||||
}
|
||||
}
|
||||
|
||||
// replayd keys ScreenCaptureKit clients by executable path, so the check runs from its own copy beside client_hands' byre-hands.
|
||||
func checkLocalPermissions() async throws -> [Permission] {
|
||||
let dir = FileManager.default.temporaryDirectory.appending(path: "byre-hands-check-\(UUID().uuidString)")
|
||||
|
||||
Reference in New Issue
Block a user