Add grep and server read/write/edit tools for agents
This commit is contained in:
+8
-1
@@ -378,7 +378,7 @@ Answers reach the agent as one message, listing each answer under the question i
|
||||
|
||||
Task agents use the Macs the human has byre's app open on directly, with no approval, as they use the machine byre runs on. While it runs, the app (`mac/Sources/Byre/Remote.swift`, started with the global event stream and stopped with it) keeps a WebSocket open to `GET /api/clients/connect`, reconnecting 2s after it drops. The connection is keyed by its `Byre-Client` header and named by its `Byre-Host` header (both required); a newer connection with the same `Byre-Client` replaces the older one, while two apps open on one Mac each keep their own. The client tools take the host name, or the `id` from `list_clients`, which they need when more than one app is connected from that host (the call fails listing the ids). byre keeps the connections in memory (`internal/clients`), pinging each every 30s and dropping one that doesn't answer within 15s. The app pings too, every 10s, and reconnects when it has had no pong for 25s. That catches a socket byre dropped while the Mac slept: the close never reached the Mac, and without something sent on it the app would wait on it indefinitely. Over it byre sends `ClientRequest` text messages, each with a fresh `id`, and the app answers each with a `ClientReply` carrying that `id` (`error` when it couldn't carry it out), in any order, handling requests concurrently. A request whose client disconnects before replying fails with "<host> disconnected before replying".
|
||||
|
||||
Implementer sessions get nine MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
|
||||
Implementer sessions get ten MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
|
||||
|
||||
- `list_clients`: the status's `clients` (see "Status"), with each one's host, login, `id`, connection time, app commit and `last_action_at`, and its iCloud sync error when its `sync` failed; the most recently active is listed first and marked.
|
||||
- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c <command>` in the human's home directory through `Process`, which starts it in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running.
|
||||
@@ -388,6 +388,7 @@ Implementer sessions get nine MCP tools for it (`internal/agents/clients.go`). T
|
||||
- `client_read`: `op: read_file`; the app replies with the file's bytes in `data`, reading on a detached task, off the main actor. It resolves symlinks, refuses anything but a regular file (a FIFO would block the open), and reads at most 64 MiB plus one byte, refusing a longer file, so the base64 reply stays well under byre's 256 MiB read limit, past which byre would drop the connection. A `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` path comes back as an image result; other files must be UTF-8 and come back as `cat -n` style numbered lines from `offset` (default 1) for `limit` lines (default 2000), with a closing line naming the next offset when lines remain.
|
||||
- `client_write`: `op: write_file` with `path` and `data`; on a detached task, off the main actor, the app resolves symlinks, refuses an existing path that isn't a regular file (a FIFO would block the write), creates the parent directories and writes the file in place (`Data.write` without `.atomic`), so an existing file keeps its permissions.
|
||||
- `client_edit`: a `read_file`, then a `write_file` of the text with `old_string` replaced by `new_string`, refused unless `old_string` appears exactly once. Nothing locks the file or checks it is unchanged between the two, so a write in between, whether another agent's `client_edit` of the same file or the human's or an app's, is lost.
|
||||
- `client_grep`: `op: bash` running `/Applications/Byre.app/Contents/Helpers/byre grep` (see "File tools") with the call's flags, pattern and `path`, each single-quoted, `~/` left outside the quotes for the shell to expand. It waits the 2 minute timeout plus 30s, and a nonzero exit fails the call with the output. A client whose app predates `byre grep` fails with its usage line.
|
||||
|
||||
`client_read`, `client_write` and `client_edit` wait at most 2 minutes for each reply, and their paths, like `client_download`'s `path`, are absolute or start with `~/`, which the app expands.
|
||||
- `client_hands`: `op: hands`, with `tool` and `arguments` as the `hands` tools take them (see "Screen and app control"). The app runs one `byre-hands`, bundled at `Contents/Helpers/byre-hands`, shared by its connections to every server (two run from that one path would cancel each other's ScreenCaptureKit connection, see "Screen and app control"), started on first use and again after it exits, writes each call to its stdin as a `tools/call` line and replies with the line's `result`, which byre returns to the agent as the tool's result, images included (`mcp.Tool.Rich`; Codex gets them as `inputImage` data URLs). The macOS privacy grants it needs (Screen Recording, Accessibility, Automation of Orion) belong to byre's app on that Mac, since the app is the responsible process; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. When `byre-hands` exits, the calls waiting on it fail. The agent's call waits for the reply 2 minutes plus the call's `wait_ms`, twice its `timeout_ms` (which `browser` and `chrome` spend once on the load and again on `wait_for`) and every `act` step's `wait`, then fails.
|
||||
@@ -428,6 +429,12 @@ A `terminal_type` that leaves text on the input line leaves a line for the human
|
||||
|
||||
For commands that don't need the human to see them, agents have the `bash` MCP tool (`internal/agents/bash.go`), for agents whose own shell tool runs in a sandbox that gets in the way. byre runs `$SHELL -l -c <command>` itself, as the service user and outside the agent's sandbox, in the task's worktree with byre's environment plus the task's `PORT`, stdin empty, stdout and stderr into one buffer, in its own process group. The group gets `SIGKILL` after `timeout_ms` (default 120000, at most 600000) or when the call is cancelled (in a Claude session, an MCP call's context ends when Claude sends `control_cancel_request` for it, as on an interrupt, or when the session's output ends), and the reply says `killed after <timeout>`; otherwise it starts with the exit status (`exit status 1`, `signal: killed`). The output is the last 64 KiB, as raw text. Background children holding the output open are waited for at most 5s after the command exits, and the reply then says the output was cut off.
|
||||
|
||||
### File tools
|
||||
|
||||
Beside `bash`, agents get `read`, `write`, `edit` and `grep` MCP tools (`internal/agents/files.go`), which byre carries out itself, as the service user and outside the agent's sandbox, the same set the client Macs get as `client_read`, `client_write`, `client_edit` and `client_grep`. A path is relative to the task's worktree, absolute, or starts with `~/` for the service user's home; nothing confines it to the worktree, as nothing confines `bash`. `read` and `edit` share the client tools' code: images come back as images, text as numbered lines, and `edit` refuses unless `old_string` appears exactly once. All three refuse a path that isn't a regular file (a FIFO would block), and `read` and `edit` refuse a file over 64 MiB, as `client_read` does. `write` creates parent directories and writes in place, so an existing file keeps its permissions.
|
||||
|
||||
`grep` and `client_grep` run the same search, `internal/grep`, in-process on the server and as `byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH` on a client, from the `byre` binary every Byre.app carries. The pattern is Go RE2, matched line by line as each file streams through, so memory stays bounded; a line over 1 MiB isn't searched and prints as `path:line: line over 1 MiB, not searched`, and the deadline is checked before every read. A directory is walked recursively, without following symlinks, skipping `.git` entries, binary files (a NUL byte in the first 8000 bytes), unreadable files and whatever gitignore rules exclude: the repository's `info/exclude` (in a linked worktree, whose `.git` is a file, the one in the shared git directory its `gitdir` and `commondir` lead to) and every `.gitignore` from the repository root (the nearest ancestor holding `.git`, or the searched directory outside a repository) down, matched with go-git's `plumbing/format/gitignore`. The global `core.excludesFile` is not read. A `path` that is itself ignored, or inside an ignored directory, is searched anyway, minus what `.gitignore` files inside it exclude. `--glob` matches file names only. Matches print as `path:line:text`, the path as given joined with the file's path below it, lines cut at 500 bytes, and output stops at 64 KiB with a closing line saying so; no match prints `No matches.`. The server `grep` gives up after 2 minutes.
|
||||
|
||||
The **Terminal** tab (`mac/Sources/Byre/Terminal.swift`) draws it with SwiftTerm's `TerminalView`. The terminal uses the UI font and is coloured from the palette (background, text, accent cursor, accent-tint selection); ANSI colours are SwiftTerm's defaults. The colours are set once per light/dark scheme (`TerminalSession.scheme`), never on other SwiftUI updates, since each colour set makes SwiftTerm redraw the whole screen and the tab re-renders on every task change in the store. It fits the pane and resizes with it. Opening the tab, or selecting another task while it is open, opens one socket for the selected task; switching tab or task closes it, and the shell keeps running, so coming back replays its recent output. A socket that drops for any other reason reconnects after 2s while the task is active; after a byre restart that starts a new shell. When the shell exits the header shows `shell ended: <status>` and a **New shell** button; when it can't be started (the socket closes with 1011, the error in byre's log as `start terminal`), the header says so, with the same button to try again. Ctrl key combinations typed in the terminal go to the shell, not to byre's Ctrl-K, Ctrl-P, Ctrl-L and Ctrl-S shortcuts; ⌘K, ⌘P, ⌘L and ⌘S still work.
|
||||
|
||||
## Snapshots
|
||||
|
||||
Reference in New Issue
Block a user