From 602e6703b49b281e68e27ab994a38e2f9bbaae35 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 18 Oct 2026 22:17:54 -0700 Subject: [PATCH] Server setup in the app --- README.md | 2 +- TODO.md | 2 - api/openapi.yaml | 26 ++ cmd/byre/check.go | 101 +++++ cmd/byre/install.go | 1 + cmd/byre/main.go | 4 +- docs/operations.md | 6 +- hands/Sources/byre-hands/Check.swift | 66 +++ hands/Sources/byre-hands/Server.swift | 4 + internal/agents/hands.go | 66 ++- internal/api/api.gen.go | 560 ++++++++++++++------------ internal/server/server.go | 9 + internal/tailscale/tailscale.go | 26 ++ mac/Sources/Byre/Server.swift | 191 ++++++++- mac/Sources/Byre/Settings.swift | 5 + mac/Sources/Byre/Startup.swift | 25 +- 16 files changed, 825 insertions(+), 269 deletions(-) create mode 100644 cmd/byre/check.go create mode 100644 hands/Sources/byre-hands/Check.swift diff --git a/README.md b/README.md index 6783dce..cad888f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ macOS, [Tailscale](https://tailscale.com) on every Mac, and on the Mac that runs ## Installing 1. Download the latest `byre-.dmg` from [the releases page](https://g.fc.run/ai/byre/releases) and drag `Byre.app` to Applications, on every Mac you'll use byre from. -2. On the Mac that runs the server, open byre and click **Run byre on this Mac**. It installs the server as a launchd service, using your login shell's `PATH` to find `claude`, `codex`, `agy`, `git` and `npx`, serves it through `tailscale serve`, and connects to it. The Server tab shows its URL and turns it off and on. +2. On the Mac that runs the server, open byre and click **Run byre on this Mac**. It first checks for `git`, Tailscale (signed in, with HTTPS certificates turned on) and at least one of `claude`, `codex` and `agy`, and lists anything missing with how to install it. Then it installs the server as a launchd service, using your login shell's `PATH` to find `claude`, `codex`, `agy`, `git` and `npx`, serves it through `tailscale serve`, and connects to it. The Server tab shows its URL, turns it off and on, and lists the tools it found and how to install the missing ones. 3. Open byre on each other Mac; it picks up the server's URL through iCloud, or enter that URL. The app updates itself, and the server along with it on the server's Mac. To build from source instead, see "Running as a service" and "macOS app" in [docs/operations.md](docs/operations.md); that needs Go and Xcode. diff --git a/TODO.md b/TODO.md index e3159c5..0aa71db 100644 --- a/TODO.md +++ b/TODO.md @@ -26,5 +26,3 @@ - Phone app A native client for the phone over the same API, sharing the macOS client's SwiftUI code where it can. Every surveyed tool has one (Claude, Codex, Cursor and Conductor apps, GitHub Mobile, Superset for iPhone); it needs its own layout, since two half-width panes don't fit a phone. -- Server setup in the app - Setting up the server's Mac from the app instead of a terminal: on the connection screen, a choice to run the server on this Mac, which does what `Contents/Helpers/byre install` does (the launch agent, `tailscale serve`), checks that `claude`, `codex`, `git` and Tailscale are there, and connects to the URL it set up. diff --git a/api/openapi.yaml b/api/openapi.yaml index 84d30b1..6b2be95 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1043,6 +1043,20 @@ paths: application/json: schema: { $ref: "#/components/schemas/Status" } + /api/admin/permissions: + post: + operationId: checkPermissions + description: Tests the service's macOS privacy grants by running byre-hands' check, which asks macOS for any grant not yet decided. Waits while macOS shows an Automation prompt, up to 50 seconds. + responses: + "200": + description: One result per grant. + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/Permission" } + "500": { $ref: "#/components/responses/Error" } + /api/workflows: get: operationId: listWorkflows @@ -1755,6 +1769,18 @@ components: cols: { type: integer, minimum: 1 } rows: { type: integer, minimum: 1 } + Permission: + type: object + required: [name, status, detail, settings] + properties: + name: { type: string, description: "The privacy grant, such as Screen Recording." } + status: + type: string + enum: [granted, missing, unchecked] + description: unchecked when the test couldn't run, such as Automation of Orion while Orion isn't running. + detail: { type: string, description: "What the test did or why it failed, and what to switch on." } + settings: { type: string, description: The x-apple.systempreferences URL of the grant's System Settings pane. } + Status: type: object required: [started_at, db_bytes, last_event_seq, projects, active_tasks, sse_clients, clients, live_agents, running_turns, rate_limits, agent_processes, zombies, stuck_after_seconds, protocol_errors] diff --git a/cmd/byre/check.go b/cmd/byre/check.go new file mode 100644 index 0000000..a8063cc --- /dev/null +++ b/cmd/byre/check.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + + "g.fc.run/ai/byre/internal/tailscale" +) + +const ( + needRequired = "required" + needAgent = "agent" + needOptional = "optional" +) + +type check struct { + Name string `json:"name"` + Need string `json:"need"` + For string `json:"for"` + OK bool `json:"ok"` + Fix string `json:"fix"` + Blocking bool `json:"blocking"` +} + +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"), + 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"} + 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) + return c +} + +func tailscaleCheck() check { + problem := tailscale.Problem(context.Background()) + 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 { + _, err := exec.LookPath(name) + return check{Name: name, Need: need, For: purpose, OK: err == nil, Fix: fix} +} + +func appCheck(name, need, purpose, fix string) check { + _, err := os.Stat("/Applications/" + name + ".app") + return check{Name: name, Need: need, For: purpose, OK: err == nil, Fix: fix} +} + +func markBlocking(cs []check) bool { + agentOK := false + for _, c := range cs { + agentOK = agentOK || (c.Need == needAgent && c.OK) + } + ready := true + for i, c := range cs { + cs[i].Blocking = !c.OK && (c.Need == needRequired || (c.Need == needAgent && !agentOK)) + ready = ready && !cs[i].Blocking + } + return ready +} + +func checkCmd() { + cs := checks() + ready := markBlocking(cs) + out, err := json.Marshal(struct { + Ready bool `json:"ready"` + Checks []check `json:"checks"` + }{ready, cs}) + if err != nil { + fatal("encode checks", "err", err) + } + fmt.Println(string(out)) +} + +func requireChecks() { + cs := checks() + if markBlocking(cs) { + return + } + 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) + } + } + os.Exit(1) +} diff --git a/cmd/byre/install.go b/cmd/byre/install.go index 30bda96..c9b83a0 100644 --- a/cmd/byre/install.go +++ b/cmd/byre/install.go @@ -44,6 +44,7 @@ const plistTemplate = ` ` func installCmd() { + requireChecks() exe, err := os.Executable() if err != nil { fatal("find executable", "err", err) diff --git a/cmd/byre/main.go b/cmd/byre/main.go index 13c9ce2..6f617a0 100644 --- a/cmd/byre/main.go +++ b/cmd/byre/main.go @@ -16,6 +16,8 @@ func main() { serveCmd(os.Args[2:]) case "install": installCmd() + case "check": + checkCmd() case "run": runCmd(os.Args[2:]) default: @@ -24,7 +26,7 @@ func main() { } func usage() { - fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre run [--resume ID]") + fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre run [--resume ID]") os.Exit(2) } diff --git a/docs/operations.md b/docs/operations.md index 2a1710a..e6bb4e5 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -115,7 +115,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 install` (`cmd/byre/install.go`) 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 ` 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://` through the Tailscale app's CLI (`tailscale.ServeRoot`, see "Previews"), printing `byre is at https://..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, 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 ` 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://` through the Tailscale app's CLI (`tailscale.ServeRoot`, see "Previews"), printing `byre is at https://..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://..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. @@ -167,6 +167,8 @@ Implementer sessions get `byre-hands` as a stdio MCP server named `hands` (`hand **Permissions.** Agents' processes, `byre-hands` included, run as byre's descendants, so macOS privacy controls (TCC) hold byre responsible for them: the Screen Recording, Accessibility and Automation grants in System Settings → Privacy & Security are byre's, made by clicking at the Mac's screen, and each app scripted for the first time asks for its own Automation grant there. `ax_tree` and `act` check `AXIsProcessTrusted` first and fail saying byre lacks Accessibility when it isn't granted, since an untrusted process otherwise just sees no windows and no menu bar. Accessibility can list byre switched off; only its switch being on counts. `codesign` gives byre the identifier `run.fc.byre` and the team's certificate (Apple Development from a checkout, Developer ID in a release), which TCC keys grants on, so they carry over from build to build, but not between a checkout's build and a release's. `codesign -dv ` shows the signature a binary has, and `launchctl print gui/$(id -u)/run.fc.byre` which binary the service runs. +`byre-hands check` (`hands/Sources/byre-hands/Check.swift`) tests the grants of whichever process started it and prints a JSON array, one row per grant with `name`, `status` (`granted`, `missing` or `unchecked`), `detail` and `settings` (the grant's `x-apple.systempreferences:` pane URL), then exits. Screen Recording lists the shareable displays and windows (waking the display first, as tool calls do), and on failure calls `CGRequestScreenCaptureAccess`. Accessibility calls `AXIsProcessTrustedWithOptions` with the prompt option and, when trusted, reads Finder's menu bar. Both requests ask macOS to show its prompt and put byre in the pane's list, switched off. Automation of Orion is `unchecked` while Orion isn't running, since asking it would launch it; otherwise `AEDeterminePermissionToAutomateTarget`, asking the user if needed (which waits for the answer), then `count windows` through AppleScript. Mouse and keyboard events need only Accessibility, and `chrome` uses Chrome's debugging port, which needs no grant. `POST /api/admin/permissions` runs it as the service: `CheckPermissions` in `internal/agents/hands.go` copies `byre-hands` from `PATH` to a new `check-*` directory under `hands/` in the data directory (its own path, for replayd), runs it with a 50s limit, under the app's 60s request timeout, and removes the directory. + ## Screenshots from implementers Each implementer session gets a media folder, `-media` (`MediaDir` in `internal/agents/hands.go`), created when the session starts and removed when the task is merged or discarded (`RemoveTask`). It sits outside the worktree, so nothing in it is committed. `byre-hands` gets it as `--media-dir` (for `save` on `capture` and `chrome`), and implementers also get the reviewers' Playwright server (`playwrightMCP`, see "Reviews") with it as `--output-dir`. The guidance (`screenshotGuidance`) tells implementers that when a turn changes something visible they can embed screenshots in the final message as Markdown images with the paths those tools return; `GET /api/tasks/{taskId}/image` serves the media folder as well as the worktree, and refuses (409) a closed task, so a closed task's screenshots show as "image not available". @@ -277,7 +279,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 four tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Status, Server and Settings, and at the end of its tab strip the connection dot and the user's name. 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//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. 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. It reads the state again each time it shows and when the app becomes active. Settings holds the user's and this client's preferences. Which left tab is selected is kept only in memory (`Overlays`); the Lead view stays mounted while another tab shows, and 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, Questions, Reviews, Workflow and Project, the lead's `tabs` first. Board shows only on a team task (see "Teams"), Reviews only once it has a `review.*` event, Inbox only while the task has open items, 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, and More carries the Questions highlight and count when Questions is in it. 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 four tabs: Lead, the lead's transcript and composer (the same view as a task transcript, with only the Compact button in its header), Status, Server and Settings, and at the end of its tab strip the connection dot and the user's name. 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//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 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, Questions, Reviews, Workflow and Project, the lead's `tabs` first. Board shows only on a team task (see "Teams"), Reviews only once it has a `review.*` event, Inbox only while the task has open items, 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, and More carries the Questions highlight and count when Questions is in it. 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 diff --git a/hands/Sources/byre-hands/Check.swift b/hands/Sources/byre-hands/Check.swift new file mode 100644 index 0000000..2635bbb --- /dev/null +++ b/hands/Sources/byre-hands/Check.swift @@ -0,0 +1,66 @@ +import AppKit +import ApplicationServices +import ScreenCaptureKit + +let orionBundle = "com.kagi.kagimacOS" + +func permission(_ name: String, _ status: String, _ detail: String, _ pane: String) -> [String: String] { + return ["name": name, "status": status, "detail": detail, "settings": "x-apple.systempreferences:com.apple.preference.security?Privacy_\(pane)"] +} + +func checkScreenRecording() async -> [String: String] { + do { + let content = try await withDisplayAwake { try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) } + return permission("Screen Recording", "granted", "Listed the screen (displays: \(content.displays.count), windows: \(content.windows.count)).", "ScreenCapture") + } catch { + CGRequestScreenCaptureAccess() + return permission("Screen Recording", "missing", "Listing the screen failed (\(error.localizedDescription)). Switch byre on in Screen Recording.", "ScreenCapture") + } +} + +func checkAccessibility() -> [String: String] { + if !AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary) { + return permission("Accessibility", "missing", "Switch byre on in Accessibility.", "Accessibility") + } + guard let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else { + return permission("Accessibility", "unchecked", "Granted, but Finder isn't running to read its menu bar.", "Accessibility") + } + do { + guard let bar = try attr(appElement(finder.processIdentifier), kAXMenuBarAttribute) else { + return permission("Accessibility", "missing", "Granted, but reading Finder's menu bar returned nothing. Switch byre off and on again in Accessibility.", "Accessibility") + } + return permission("Accessibility", "granted", "Read Finder's menu bar (menus: \(try children(bar as! AXUIElement).count)).", "Accessibility") + } catch let e as ToolError { + return permission("Accessibility", "missing", e.message, "Accessibility") + } catch { + return permission("Accessibility", "missing", "\(error)", "Accessibility") + } +} + +func checkOrion() -> [String: String] { + if NSRunningApplication.runningApplications(withBundleIdentifier: orionBundle).isEmpty { + return permission("Automation of Orion", "unchecked", "Orion isn't running; open it and check again.", "Automation") + } + let target = NSAppleEventDescriptor(bundleIdentifier: orionBundle) + let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true) + if status != noErr { + return permission("Automation of Orion", "missing", "macOS refused Apple events to Orion (\(status)). Switch Orion on under byre in Automation.", "Automation") + } + do { + let windows = try osa("tell application \"Orion\" to count windows").int32Value + return permission("Automation of Orion", "granted", "Orion answered (windows: \(windows)).", "Automation") + } catch let e as ToolError { + return permission("Automation of Orion", "missing", e.message, "Automation") + } catch { + return permission("Automation of Orion", "missing", "\(error)", "Automation") + } +} + +func runCheck() { + Task.detached { + let rows = [await checkScreenRecording(), checkAccessibility(), checkOrion()] + let out = try! JSONSerialization.data(withJSONObject: rows, options: .withoutEscapingSlashes) + FileHandle.standardOutput.write(out + Data("\n".utf8)) + exit(0) + } +} diff --git a/hands/Sources/byre-hands/Server.swift b/hands/Sources/byre-hands/Server.swift index 418a930..0f088dd 100644 --- a/hands/Sources/byre-hands/Server.swift +++ b/hands/Sources/byre-hands/Server.swift @@ -45,6 +45,10 @@ struct Server { _ = NSApplication.shared NSApp.setActivationPolicy(.prohibited) AXUIElementSetMessagingTimeout(AXUIElementCreateSystemWide(), 3) + if CommandLine.arguments.dropFirst().first == "check" { + runCheck() + RunLoop.main.run() + } Thread.detachNewThread { let inFlight = DispatchGroup() while let line = readLine() { diff --git a/internal/agents/hands.go b/internal/agents/hands.go index 5fe3c97..f448c9b 100644 --- a/internal/agents/hands.go +++ b/internal/agents/hands.go @@ -1,10 +1,16 @@ package agents import ( + "context" + "encoding/json" + "fmt" "os" "os/exec" "path/filepath" + "strings" + "time" + "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/mcp" ) @@ -21,16 +27,8 @@ func (m *Manager) handsDir(sessionID string) string { // replayd keys ScreenCaptureKit clients by executable path, so two hands // processes run from one path keep cancelling each other's connection. func (m *Manager) handsMCP(sessionID, mediaDir string) (mcp.Stdio, error) { - src, err := exec.LookPath("byre-hands") - if err != nil { - return mcp.Stdio{}, err - } - data, err := os.ReadFile(src) - if err != nil { - return mcp.Stdio{}, err - } dir := m.handsDir(sessionID) - err = os.RemoveAll(dir) + err := os.RemoveAll(dir) if err != nil { return mcp.Stdio{}, err } @@ -38,10 +36,56 @@ func (m *Manager) handsMCP(sessionID, mediaDir string) (mcp.Stdio, error) { if err != nil { return mcp.Stdio{}, err } - bin := filepath.Join(dir, "byre-hands") - err = os.WriteFile(bin, data, 0o755) + bin, err := copyHands(dir) if err != nil { return mcp.Stdio{}, err } return mcp.Stdio{Name: "hands", Command: bin, Args: []string{"--media-dir", mediaDir}}, nil } + +func copyHands(dir string) (string, error) { + src, err := exec.LookPath("byre-hands") + if err != nil { + return "", err + } + data, err := os.ReadFile(src) + if err != nil { + return "", err + } + bin := filepath.Join(dir, "byre-hands") + return bin, os.WriteFile(bin, data, 0o755) +} + +func (m *Manager) CheckPermissions(ctx context.Context) ([]api.Permission, error) { + err := os.MkdirAll(m.handsRoot, 0o755) + if err != nil { + return nil, err + } + dir, err := os.MkdirTemp(m.handsRoot, "check-") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + bin, err := copyHands(dir) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(ctx, 50*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, bin, "check") + stderr := strings.Builder{} + cmd.Stderr = &stderr + out, err := cmd.Output() + if ctx.Err() == context.DeadlineExceeded { + return nil, fmt.Errorf("byre-hands check didn't finish within 50s; answer macOS's prompt on the server's Mac and check again") + } + if err != nil { + return nil, fmt.Errorf("byre-hands check: %w: %s", err, strings.TrimSpace(stderr.String())) + } + perms := []api.Permission{} + err = json.Unmarshal(out, &perms) + if err != nil { + return nil, fmt.Errorf("parse byre-hands check: %w", err) + } + return perms, nil +} diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 3784cb6..64495fa 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -276,6 +276,27 @@ func (e InboxState) Valid() bool { } } +// Defines values for PermissionStatus. +const ( + Granted PermissionStatus = "granted" + Missing PermissionStatus = "missing" + Unchecked PermissionStatus = "unchecked" +) + +// Valid indicates whether the value is a known member of the PermissionStatus enum. +func (e PermissionStatus) Valid() bool { + switch e { + case Granted: + return true + case Missing: + return true + case Unchecked: + return true + default: + return false + } +} + // Defines values for ReviewState. const ( ReviewStateDone ReviewState = "done" @@ -968,6 +989,24 @@ type Participant struct { Login string `json:"login"` } +// Permission defines model for Permission. +type Permission struct { + // Detail What the test did or why it failed, and what to switch on. + Detail string `json:"detail"` + + // Name The privacy grant, such as Screen Recording. + Name string `json:"name"` + + // Settings The x-apple.systempreferences URL of the grant's System Settings pane. + Settings string `json:"settings"` + + // Status unchecked when the test couldn't run, such as Automation of Orion while Orion isn't running. + Status PermissionStatus `json:"status"` +} + +// PermissionStatus unchecked when the test couldn't run, such as Automation of Orion while Orion isn't running. +type PermissionStatus string + // Preview defines model for Preview. type Preview struct { CreatedAt time.Time `json:"created_at"` @@ -1574,6 +1613,9 @@ type MarkSummaryReadJSONRequestBody = SummaryRead // ServerInterface represents all server handlers. type ServerInterface interface { + // (POST /api/admin/permissions) + CheckPermissions(w http.ResponseWriter, r *http.Request) + // (GET /api/admin/status) GetStatus(w http.ResponseWriter, r *http.Request) @@ -1779,6 +1821,20 @@ type ServerInterfaceWrapper struct { type MiddlewareFunc func(http.Handler) http.Handler +// CheckPermissions operation middleware +func (siw *ServerInterfaceWrapper) CheckPermissions(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CheckPermissions(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetStatus operation middleware func (siw *ServerInterfaceWrapper) GetStatus(w http.ResponseWriter, r *http.Request) { @@ -3771,6 +3827,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/inbox/responses", wrapper.RespondInbox) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/events", wrapper.StreamEvents) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/admin/status", wrapper.GetStatus) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/admin/permissions", wrapper.CheckPermissions) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/workflows", wrapper.ListWorkflows) m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/workflows/{name}", wrapper.DeleteWorkflow) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/workflows/{name}", wrapper.GetWorkflow) @@ -3784,255 +3841,260 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "YssYaXgUony5VOY7NGNvqefefX6vIk0fP/7vAQA=", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/server.go b/internal/server/server.go index 6c874c0..8cd4e88 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -623,6 +623,15 @@ func (s *Server) GetStatus(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, status) } +func (s *Server) CheckPermissions(w http.ResponseWriter, r *http.Request) { + perms, err := s.agents.CheckPermissions(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, perms) +} + func (s *Server) status(ctx context.Context) (api.Status, error) { c, err := s.store.Counts(ctx) if err != nil { diff --git a/internal/tailscale/tailscale.go b/internal/tailscale/tailscale.go index 4595c4c..bb223d2 100644 --- a/internal/tailscale/tailscale.go +++ b/internal/tailscale/tailscale.go @@ -50,6 +50,32 @@ func serve(ctx context.Context, port int, target string) (string, error) { return "", fmt.Errorf("tailscale serve --https=%d exited 0 but port %d is not in serve status", port, port) } +func Problem(ctx context.Context) string { + _, err := os.Stat(cli) + if err != nil { + return "Install the Tailscale app from https://tailscale.com/download/mac and sign in." + } + out, err := run(ctx, "status", "--json", "--peers=false") + if err != nil { + return "Open Tailscale and sign in (" + err.Error() + ")." + } + status := struct { + BackendState string + CertDomains []string + }{} + err = json.Unmarshal(out, &status) + if err != nil { + return "Parse tailscale status: " + err.Error() + } + if status.BackendState != "Running" { + return "Open Tailscale and sign in (it is " + status.BackendState + ")." + } + if len(status.CertDomains) == 0 { + return "Turn on HTTPS Certificates on the DNS page of Tailscale's admin console, https://login.tailscale.com/admin/dns." + } + return "" +} + func Off(ctx context.Context, port int) error { _, err := run(ctx, "serve", "--https="+strconv.Itoa(port), "off") return err diff --git a/mac/Sources/Byre/Server.swift b/mac/Sources/Byre/Server.swift index d4733d7..3867700 100644 --- a/mac/Sources/Byre/Server.swift +++ b/mac/Sources/Byre/Server.swift @@ -1,6 +1,9 @@ +import ByreAPI import Foundation import SwiftUI +typealias Permission = Components.Schemas.Permission + struct LaunchAgent: Decodable { let programArguments: [String] @@ -18,6 +21,143 @@ struct ServerState { var address: String? } +struct Check: Decodable { + let name: String + let need: String + let `for`: String + let ok: Bool + let fix: String + let blocking: Bool +} + +struct Checks: Decodable { + let ready: Bool + let checks: [Check] +} + +struct ChecksView: View { + let checks: [Check] + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + ForEach(checks, id: \.name) { check in + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 8) { + Text(check.name).font(Fonts.font(13, weight: 700)) + status(check) + } + Text("\(need(check)), for \(check.for)").foregroundStyle(Palette.muted) + if !check.ok { + Text(check.fix) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + } + + private func status(_ check: Check) -> some View { + if check.ok { + return Text("Found").foregroundStyle(Palette.ok) + } + return Text("Missing").foregroundStyle(check.blocking ? Palette.danger : Palette.muted) + } + + private func need(_ check: Check) -> String { + switch check.need { + case "required": + return "Required" + case "agent": + return "One of claude, codex and agy required" + default: + return "Optional" + } + } +} + +// 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)") + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: dir) + } + let bin = dir.appending(path: "byre-hands") + try FileManager.default.copyItem(at: Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre-hands"), to: bin) + let r = try await execute(bin.path, ["check"]) + guard r.status == 0, let perms = try? JSONDecoder().decode([Permission].self, from: Data(r.output.utf8)) else { + throw ProcessFailure(errorDescription: "byre-hands check failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") + } + return perms +} + +struct PermissionsCheck: View { + let run: () async throws -> [Permission] + @ViewState private var result: Result<[Permission], any Error>? + @ViewState private var busy = false + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Button(busy ? "Checking…" : "Check permissions", action: check) + .buttonStyle(WebButtonStyle()) + .disabled(busy) + switch result { + case .success(let perms): + ForEach(perms, id: \.name) { perm in + row(perm) + } + case .failure(let error): + Text(errorText(error)) + .foregroundStyle(Palette.danger) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + case nil: + EmptyView() + } + } + .padding(.top, 4) + } + + private func row(_ perm: Permission) -> some View { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 8) { + Text(perm.name).font(Fonts.font(13, weight: 700)) + switch perm.status { + case .granted: + Text("Granted").foregroundStyle(Palette.ok) + case .missing: + Text("Missing").foregroundStyle(Palette.danger) + case .unchecked: + Text("Not checked").foregroundStyle(Palette.muted) + } + } + Text(perm.detail) + .foregroundStyle(Palette.muted) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + if perm.status == .missing, let url = URL(string: perm.settings) { + Button("Open System Settings") { + NSWorkspace.shared.open(url) + } + .buttonStyle(SmallLinkButtonStyle()) + } + } + } + + private func check() { + busy = true + Task { + do { + result = .success(try await run()) + } catch { + result = .failure(error) + } + busy = false + } + } +} + struct ProcessFailure: LocalizedError { let errorDescription: String? } @@ -65,9 +205,21 @@ enum Server { } // byre install copies its PATH into the service, which needs the login shell's to find claude, codex, agy and npx. - static func install() async throws -> URL { + private static func helper(_ command: String) async throws -> (status: Int32, output: String) { let shell = unsafe String(cString: getpwuid(getuid()).pointee.pw_shell) - let r = try await execute(shell, ["-l", "-c", "exec \"$BYRE_HELPER\" install"], env: ["BYRE_HELPER": serverHelper]) + return try await execute(shell, ["-l", "-c", "exec \"$BYRE_HELPER\" \(command)"], env: ["BYRE_HELPER": serverHelper]) + } + + static func checks() async throws -> Checks { + let r = try await helper("check") + guard r.status == 0, let line = r.output.split(separator: "\n").last, let checks = try? JSONDecoder().decode(Checks.self, from: Data(line.utf8)) else { + throw ProcessFailure(errorDescription: "byre check failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") + } + return checks + } + + static func install() async throws -> URL { + let r = try await helper("install") let prefix = "byre is at " guard r.status == 0, let line = r.output.split(separator: "\n").first(where: { $0.hasPrefix(prefix) }), let url = URL(string: String(line.dropFirst(prefix.count))) else { throw ProcessFailure(errorDescription: "byre install failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") @@ -92,6 +244,7 @@ enum Server { struct ServerView: View { @Environment(Overlays.self) private var overlays @ViewState private var state: ServerState? + @ViewState private var checks: Result? @ViewState private var busy = false var body: some View { @@ -103,6 +256,30 @@ struct ServerView: View { if let state { details(state) } + Text("Tools on this Mac") + .font(Fonts.font(13, weight: 700)) + .padding(.top, 16) + switch checks { + case .success(let checks): + ChecksView(checks: checks.checks) + case .failure(let error): + Text(errorText(error)) + .foregroundStyle(Palette.danger) + .textSelection(.enabled) + case nil: + EmptyView() + } + if let address = state?.address, let origin = URL(string: address) { + Text("Agents' permissions") + .font(Fonts.font(13, weight: 700)) + .padding(.top, 16) + Text("The macOS privacy grants the server's agents use to see and control this Mac. Checking asks macOS for any that are missing.") + .foregroundStyle(Palette.muted) + .fixedSize(horizontal: false, vertical: true) + PermissionsCheck { + try await API(origin: origin).client.checkPermissions().ok.body.json + } + } } .padding(.horizontal, 16) .padding(.top, 8) @@ -156,6 +333,16 @@ struct ServerView: View { } private func refresh() async { + async let checked = check() state = await Server.state() + checks = await checked + } + + private func check() async -> Result { + do { + return .success(try await Server.checks()) + } catch { + return .failure(error) + } } } diff --git a/mac/Sources/Byre/Settings.swift b/mac/Sources/Byre/Settings.swift index d031030..9b718f4 100644 --- a/mac/Sources/Byre/Settings.swift +++ b/mac/Sources/Byre/Settings.swift @@ -35,6 +35,11 @@ struct SettingsView: View { .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) DefaultModelMenu() + heading("This Mac's permissions") + Text("The macOS privacy grants byre's app needs for agents to see and control this Mac. Checking asks macOS for any that are missing.") + .foregroundStyle(Palette.muted) + .fixedSize(horizontal: false, vertical: true) + PermissionsCheck(run: checkLocalPermissions) heading("What your lead has learned") Text("Your lead notes your priorities and preferences as it works with you, and starts every session with them.") .foregroundStyle(Palette.muted) diff --git a/mac/Sources/Byre/Startup.swift b/mac/Sources/Byre/Startup.swift index 6d55063..faa5111 100644 --- a/mac/Sources/Byre/Startup.swift +++ b/mac/Sources/Byre/Startup.swift @@ -199,6 +199,7 @@ final class Connection { final class ConnectForm { var serverText = "" var problem: String? + var missing: [Check] = [] var connecting = false func connect() async -> URL? { @@ -223,7 +224,14 @@ final class ConnectForm { func installHere() async -> URL? { connecting = true problem = nil + missing = [] do { + let checks = try await Server.checks() + if !checks.ready { + missing = checks.checks.filter { !$0.ok } + connecting = false + return nil + } let origin = try await Server.turnOn() serverText = origin.absoluteString try await waitForServer(origin) @@ -294,6 +302,16 @@ struct ConnectView: View { let connected: (URL) -> Void var body: some View { + GeometryReader { g in + ScrollView { + fields + .padding(.vertical, 24) + .frame(maxWidth: .infinity, minHeight: g.size.height) + } + } + } + + private var fields: some View { VStack(alignment: .leading, spacing: 10) { Text("Server URL") TextField("https://byre.example.ts.net", text: $form.serverText) @@ -310,9 +328,14 @@ struct ConnectView: View { .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } + 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.") + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 6) + ChecksView(checks: form.missing) + } } .frame(width: 460) - .frame(maxWidth: .infinity, maxHeight: .infinity) } private func connect() {