diff --git a/api/openapi.yaml b/api/openapi.yaml index c94e225..8807c1c 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -928,13 +928,24 @@ paths: - { name: transferId, in: path, required: true, schema: { type: string } } get: operationId: getClientTransfer - description: The file or directory a copy ClientRequest sends, as a gzipped tar holding it under its own name. Served while the request is waiting for its reply. + description: The file or directory an upload ClientRequest sends, as a gzipped tar holding it under its own name. Served while the request is waiting for its reply. responses: "200": description: The archive. content: application/gzip: { schema: { type: string, format: binary } } "404": { $ref: "#/components/responses/Error" } + put: + operationId: putClientTransfer + description: The file or directory a download ClientRequest asks for, as a gzipped tar holding it under its own name. Accepted once, while the request is waiting for its reply. + requestBody: + required: true + content: + application/gzip: { schema: { type: string, format: binary } } + responses: + "204": + description: Received. + "404": { $ref: "#/components/responses/Error" } /api/inbox/responses: post: @@ -1718,11 +1729,13 @@ components: description: Work a task agent asks a connected Mac to do, sent over /api/clients/connect. properties: id: { type: string } - op: { type: string, enum: [bash, copy, hands] } + op: { type: string, enum: [bash, upload, download, read_file, write_file, hands] } command: { type: string, description: "bash: run with zsh as a login shell in the home directory, in its own process group." } timeout_ms: { type: integer, description: "bash: kill the process group after this long." } - transfer: { type: string, description: "copy: the transfer id to download from /api/clients/transfers/{transferId}." } - dest: { type: string, description: "copy: where the archive's one entry ends up, absolute or starting with ~/; whatever is there is replaced." } + transfer: { type: string, description: "upload: the transfer id to get from /api/clients/transfers/{transferId}. download: the transfer id to put the archive to." } + dest: { type: string, description: "upload: where the archive's one entry ends up, absolute or starting with ~/; whatever is there is replaced." } + path: { type: string, description: "download, read_file, write_file: the file (or, for download, file or directory) on the client, absolute or starting with ~/." } + data: { type: string, format: byte, description: "write_file: the file's new contents; parent directories are created." } tool: { type: string, description: "hands: the byre-hands tool to call." } arguments: type: object @@ -1737,6 +1750,7 @@ components: error: { type: string, description: Set when the request couldn't be carried out. } exit_code: { type: integer, description: "bash: the exit status, -1 when it was killed by its timeout." } output: { type: string, description: "bash: stdout and stderr together, at most their last 64 KiB." } + data: { type: string, format: byte, description: "read_file: the file's contents." } result: type: object additionalProperties: true diff --git a/docs/operations.md b/docs/operations.md index 2d0cbf6..42666cd 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -298,14 +298,20 @@ Answers reach the agent as one message, listing each answer under the question o 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 client is named by its `Byre-Host` header (required), and a newer connection from the same host replaces the older one. byre keeps the connections in memory (`internal/clients`), pinging each every 30s and dropping one that doesn't answer within 15s. 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 " disconnected before replying". -Implementer sessions get four MCP tools for it (`internal/agents/clients.go`): +Implementer sessions get eight 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 connected clients, with each one's login, connection time and app commit, and its `last_action_at` from Status (see "Status") matched on login, host and commit; the most recently active is listed first and marked. - `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c ` in the human's home directory through `posix_spawn` 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. -- `client_copy`: `op: copy`. `path` is checked against the worktree through an `os.Root`; byre registers a transfer naming the path's directory and base name under a random id for as long as the call waits (at most 10 minutes), and the app downloads `GET /api/clients/transfers/{transferId}`, which streams `tar -czf - -C -- ` (404 once the call has ended). The app unpacks it into a new `.byre-copy-` directory beside `dest` (`~/` expanded, parent directories created), removes whatever is at `dest`, moves the archive's one entry there and removes the temporary directory. +- `client_upload`: `op: upload`. `path` is checked against the worktree through an `os.Root`; byre registers a transfer naming the path's directory and base name under a random id for as long as the call waits (at most 10 minutes), and the app downloads `GET /api/clients/transfers/{transferId}`, which streams `tar -czf - -C -- ` (404 once the call has ended). The app unpacks it into a new `.byre-upload-` directory beside `dest` (`~/` expanded, parent directories created), removes whatever is at `dest`, moves the archive's one entry there and removes the temporary directory. +- `client_download`: `op: download`, the reverse. byre makes `dest`'s parent directories through an `os.Root` on the worktree and registers a receiving transfer (`Transfer.Receive`) for a fresh temporary directory; the app runs `tar -czf -C -- ` and `PUT`s it to `/api/clients/transfers/{transferId}`, which writes it once (`O_EXCL`) and answers 204, and 404 for a sending transfer, one already received or one whose call has ended (`GET` likewise answers 404 for a receiving one). byre unpacks it into a new `.byre-download-` directory beside `dest`, removes whatever is at `dest`, renames the archive's one entry there and removes the temporary directories, all inside the `os.Root`. +- `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_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/MacOS/byre-hands`, 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. Each `byre-hands` process has its own pending calls, so one that exits fails only the calls sent to it. -A byre restart drops every connection; each app reconnects 2s later, and calls in flight at the restart fail as disconnected. Updating a client's app goes through the tools themselves: `client_copy` the new `byre.app`, then `client_bash` a command that quits the old app and opens the new one, which outlives the app and reconnects. An app built before these tools existed never connects, so its first update is by hand. +A byre restart drops every connection; each app reconnects 2s later, and calls in flight at the restart fail as disconnected. Updating a client's app goes through the tools themselves: `client_upload` the new `byre.app`, then `client_bash` a command that quits the old app and opens the new one, which outlives the app and reconnects. An app that doesn't know the `upload` op can't be updated this way, so its update is by hand. `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` on a client shows `client request : ` lines, connection drops and `byre-hands` starts and exits; byre's log shows `client connected` and `client disconnected`. diff --git a/internal/agents/clients.go b/internal/agents/clients.go index 1c747f0..f50d132 100644 --- a/internal/agents/clients.go +++ b/internal/agents/clients.go @@ -2,14 +2,18 @@ package agents import ( "context" + "encoding/base64" "encoding/json" "errors" "fmt" + "io/fs" "os" + "os/exec" "path/filepath" "sort" "strings" "time" + "unicode/utf8" "g.fc.run/misc/byre/internal/api" "g.fc.run/misc/byre/internal/clients" @@ -21,7 +25,9 @@ const ( maxBashTimeout = 10 * time.Minute replyGrace = 30 * time.Second copyTimeout = 10 * time.Minute + fileTimeout = 2 * time.Minute handsTimeout = 11 * time.Minute + defaultReadLimit = 2000 ) func (m *Manager) clientTools(l *live) []mcp.Tool { @@ -55,7 +61,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool { }, }, { - Name: "client_copy", + Name: "client_upload", Description: "Copy a file or directory from your worktree to a client Mac, replacing whatever is at dest there. " + "Directories, app bundles included, are copied whole with their permissions and symlinks.", InputSchema: json.RawMessage(`{ @@ -68,7 +74,76 @@ func (m *Manager) clientTools(l *live) []mcp.Tool { "required": ["client", "path", "dest"] }`), Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { - return m.clientCopy(ctx, l, raw) + return m.clientUpload(ctx, l, raw) + }, + }, + { + Name: "client_download", + Description: "Copy a file or directory from a client Mac into your worktree, replacing whatever is at dest there. " + + "Directories are copied whole with their permissions and symlinks.", + InputSchema: json.RawMessage(`{ + "type": "object", + "properties": { + "client": {"type": "string", "description": "The client's name from list_clients."}, + "path": {"type": "string", "description": "The file or directory on the client, absolute or starting with ~/."}, + "dest": {"type": "string", "description": "Its new path, relative to your worktree; parent directories are created."} + }, + "required": ["client", "path", "dest"] + }`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + return m.clientDownload(ctx, l, raw) + }, + }, + { + Name: "client_read", + Description: "Read a file on a client Mac. Text comes back with line numbers, starting at line 1 (or offset), at most limit lines. " + + "A PNG, JPEG, GIF or WebP image comes back as the image. Other binary files fail; use client_download for them.", + InputSchema: json.RawMessage(`{ + "type": "object", + "properties": { + "client": {"type": "string", "description": "The client's name from list_clients."}, + "path": {"type": "string", "description": "The file, absolute or starting with ~/."}, + "offset": {"type": "integer", "description": "The line number to start from (default 1)."}, + "limit": {"type": "integer", "description": "The most lines to return (default 2000)."} + }, + "required": ["client", "path"] + }`), + Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result { + return m.clientRead(ctx, raw) + }, + }, + { + Name: "client_write", + Description: "Write a file on a client Mac, replacing its contents, creating it and its parent directories when missing.", + InputSchema: json.RawMessage(`{ + "type": "object", + "properties": { + "client": {"type": "string", "description": "The client's name from list_clients."}, + "path": {"type": "string", "description": "The file, absolute or starting with ~/."}, + "content": {"type": "string"} + }, + "required": ["client", "path", "content"] + }`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + return m.clientWrite(ctx, raw) + }, + }, + { + Name: "client_edit", + Description: "Replace old_string with new_string in a text file on a client Mac. " + + "old_string must match the file exactly, whitespace included, and appear exactly once; otherwise the file is left as it was.", + InputSchema: json.RawMessage(`{ + "type": "object", + "properties": { + "client": {"type": "string", "description": "The client's name from list_clients."}, + "path": {"type": "string", "description": "The file, absolute or starting with ~/."}, + "old_string": {"type": "string"}, + "new_string": {"type": "string"} + }, + "required": ["client", "path", "old_string", "new_string"] + }`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + return m.clientEdit(ctx, raw) }, }, { @@ -178,20 +253,28 @@ func (m *Manager) clientBash(ctx context.Context, raw json.RawMessage) (string, return status + "\n" + *reply.Output, nil } -type clientCopyArgs struct { +type clientTransferArgs struct { Client string `json:"client"` Path string `json:"path"` Dest string `json:"dest"` } -func (m *Manager) clientCopy(ctx context.Context, l *live, raw json.RawMessage) (string, error) { - args := clientCopyArgs{} +func clientPath(name, p string) error { + if !filepath.IsAbs(p) && !strings.HasPrefix(p, "~/") { + return fmt.Errorf("%s %q must be absolute or start with ~/", name, p) + } + return nil +} + +func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage) (string, error) { + args := clientTransferArgs{} err := json.Unmarshal(raw, &args) if err != nil { - return "", fmt.Errorf("malformed client_copy call: %w", err) + return "", fmt.Errorf("malformed client_upload call: %w", err) } - if !filepath.IsAbs(args.Dest) && !strings.HasPrefix(args.Dest, "~/") { - return "", fmt.Errorf("dest %q must be absolute or start with ~/", args.Dest) + err = clientPath("dest", args.Dest) + if err != nil { + return "", err } clean := filepath.Clean(args.Path) @@ -214,13 +297,233 @@ func (m *Manager) clientCopy(ctx context.Context, l *live, raw json.RawMessage) cctx, cancel := context.WithTimeout(ctx, copyTimeout) defer cancel() - _, err = m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Copy, Transfer: &id, Dest: &args.Dest}) + _, err = m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Upload, Transfer: &id, Dest: &args.Dest}) if err != nil { return "", err } return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil } +func (m *Manager) clientDownload(ctx context.Context, l *live, raw json.RawMessage) (string, error) { + args := clientTransferArgs{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", fmt.Errorf("malformed client_download call: %w", err) + } + err = clientPath("path", args.Path) + if err != nil { + return "", err + } + dest := filepath.Clean(args.Dest) + if filepath.IsAbs(dest) || dest == "." { + return "", fmt.Errorf("dest %q must be a path inside your worktree, relative to it", args.Dest) + } + + root, err := os.OpenRoot(l.dir) + if err != nil { + return "", err + } + defer root.Close() + parent := filepath.Dir(dest) + err = root.MkdirAll(parent, 0o755) + if err != nil { + return "", fmt.Errorf("dest %q: %w", args.Dest, err) + } + + tmp, err := os.MkdirTemp("", "byre-download-") + if err != nil { + return "", err + } + defer os.RemoveAll(tmp) + id := m.clients.AddTransfer(clients.Transfer{Dir: tmp, Name: "archive.tgz", Receive: true}) + defer m.clients.RemoveTransfer(id) + + cctx, cancel := context.WithTimeout(ctx, copyTimeout) + defer cancel() + _, err = m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Download, Path: &args.Path, Transfer: &id}) + if err != nil { + return "", err + } + + stage := filepath.Join(parent, ".byre-download-"+id) + err = root.Mkdir(stage, 0o700) + if err != nil { + return "", err + } + defer root.RemoveAll(stage) + out, err := exec.CommandContext(ctx, "tar", "-xzf", filepath.Join(tmp, "archive.tgz"), "-C", filepath.Join(l.dir, stage)).CombinedOutput() + if err != nil { + return "", fmt.Errorf("unpack: %w: %s", err, out) + } + entries, err := fs.ReadDir(root.FS(), stage) + if err != nil { + return "", err + } + if len(entries) != 1 { + return "", fmt.Errorf("unpack: the archive holds %d entries, not 1", len(entries)) + } + err = root.RemoveAll(dest) + if err != nil { + return "", err + } + err = root.Rename(filepath.Join(stage, entries[0].Name()), dest) + if err != nil { + return "", err + } + return fmt.Sprintf("Copied %s on %s to %s.", args.Path, args.Client, dest), nil +} + +func (m *Manager) readClientFile(ctx context.Context, client, path string) ([]byte, error) { + err := clientPath("path", path) + if err != nil { + return nil, err + } + cctx, cancel := context.WithTimeout(ctx, fileTimeout) + defer cancel() + reply, err := m.clients.Call(cctx, client, api.ClientRequest{Op: api.ReadFile, Path: &path}) + if err != nil { + return nil, err + } + if reply.Data == nil { + return nil, fmt.Errorf("%s's reply has no data", client) + } + return *reply.Data, nil +} + +func (m *Manager) writeClientFile(ctx context.Context, client, path string, data []byte) error { + err := clientPath("path", path) + if err != nil { + return err + } + cctx, cancel := context.WithTimeout(ctx, fileTimeout) + defer cancel() + _, err = m.clients.Call(cctx, client, api.ClientRequest{Op: api.WriteFile, Path: &path, Data: &data}) + return err +} + +var imageTypes = map[string]string{".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp"} + +type clientReadArgs struct { + Client string `json:"client"` + Path string `json:"path"` + Offset *int `json:"offset"` + Limit *int `json:"limit"` +} + +func (m *Manager) clientRead(ctx context.Context, raw json.RawMessage) mcp.Result { + args := clientReadArgs{} + err := json.Unmarshal(raw, &args) + if err != nil { + return mcp.TextResult(fmt.Sprintf("malformed client_read call: %s", err), true) + } + data, err := m.readClientFile(ctx, args.Client, args.Path) + if err != nil { + return mcp.TextResult(err.Error(), true) + } + mime, ok := imageTypes[strings.ToLower(filepath.Ext(args.Path))] + if ok { + return mcp.Result{Content: []mcp.Content{{Type: "image", Data: base64.StdEncoding.EncodeToString(data), MimeType: mime}}} + } + text, err := numberLines(args.Path, data, args.Offset, args.Limit) + if err != nil { + return mcp.TextResult(err.Error(), true) + } + return mcp.TextResult(text, false) +} + +func numberLines(path string, data []byte, offset, limit *int) (string, error) { + if !utf8.Valid(data) { + return "", fmt.Errorf("%s isn't text; use client_download to copy it into your worktree", path) + } + if len(data) == 0 { + return fmt.Sprintf("%s is empty.", path), nil + } + start := 1 + if offset != nil { + start = *offset + } + n := defaultReadLimit + if limit != nil { + n = *limit + } + if start < 1 || n < 1 { + return "", errors.New("offset and limit must be at least 1") + } + lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n") + if start > len(lines) { + return "", fmt.Errorf("%s has %d lines; offset %d is past the end", path, len(lines), start) + } + end := min(start-1+n, len(lines)) + b := &strings.Builder{} + for i := start - 1; i < end; i++ { + fmt.Fprintf(b, "%6d\t%s\n", i+1, lines[i]) + } + if end < len(lines) { + fmt.Fprintf(b, "(%d more lines; read on with offset %d)\n", len(lines)-end, end+1) + } + return b.String(), nil +} + +type clientWriteArgs struct { + Client string `json:"client"` + Path string `json:"path"` + Content string `json:"content"` +} + +func (m *Manager) clientWrite(ctx context.Context, raw json.RawMessage) (string, error) { + args := clientWriteArgs{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", fmt.Errorf("malformed client_write call: %w", err) + } + err = m.writeClientFile(ctx, args.Client, args.Path, []byte(args.Content)) + if err != nil { + return "", err + } + return fmt.Sprintf("Wrote %d bytes to %s on %s.", len(args.Content), args.Path, args.Client), nil +} + +type clientEditArgs struct { + Client string `json:"client"` + Path string `json:"path"` + OldString string `json:"old_string"` + NewString string `json:"new_string"` +} + +func (m *Manager) clientEdit(ctx context.Context, raw json.RawMessage) (string, error) { + args := clientEditArgs{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", fmt.Errorf("malformed client_edit call: %w", err) + } + if args.OldString == "" { + return "", errors.New("old_string is empty") + } + if args.OldString == args.NewString { + return "", errors.New("old_string and new_string are the same") + } + data, err := m.readClientFile(ctx, args.Client, args.Path) + if err != nil { + return "", err + } + if !utf8.Valid(data) { + return "", fmt.Errorf("%s isn't text", args.Path) + } + text := string(data) + n := strings.Count(text, args.OldString) + if n == 0 { + return "", fmt.Errorf("old_string isn't in %s", args.Path) + } + if n > 1 { + return "", fmt.Errorf("old_string appears %d times in %s; include more of the text around it so it appears once", n, args.Path) + } + err = m.writeClientFile(ctx, args.Client, args.Path, []byte(strings.Replace(text, args.OldString, args.NewString, 1))) + if err != nil { + return "", err + } + return fmt.Sprintf("Edited %s on %s.", args.Path, args.Client), nil +} + type clientHandsArgs struct { Client string `json:"client"` Tool string `json:"tool"` diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index b39436c..5efca69 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -137,9 +137,12 @@ func (e AttachmentKind) Valid() bool { // Defines values for ClientRequestOp. const ( - Bash ClientRequestOp = "bash" - Copy ClientRequestOp = "copy" - Hands ClientRequestOp = "hands" + Bash ClientRequestOp = "bash" + Download ClientRequestOp = "download" + Hands ClientRequestOp = "hands" + ReadFile ClientRequestOp = "read_file" + Upload ClientRequestOp = "upload" + WriteFile ClientRequestOp = "write_file" ) // Valid indicates whether the value is a known member of the ClientRequestOp enum. @@ -147,10 +150,16 @@ func (e ClientRequestOp) Valid() bool { switch e { case Bash: return true - case Copy: + case Download: return true case Hands: return true + case ReadFile: + return true + case Upload: + return true + case WriteFile: + return true default: return false } @@ -654,6 +663,9 @@ type Blob struct { // ClientReply defines model for ClientReply. type ClientReply struct { + // Data read_file: the file's contents. + Data *[]byte `json:"data,omitempty"` + // Error Set when the request couldn't be carried out. Error *string `json:"error,omitempty"` @@ -678,18 +690,24 @@ type ClientRequest struct { // Command bash: run with zsh as a login shell in the home directory, in its own process group. Command *string `json:"command,omitempty"` - // Dest copy: where the archive's one entry ends up, absolute or starting with ~/; whatever is there is replaced. + // Data write_file: the file's new contents; parent directories are created. + Data *[]byte `json:"data,omitempty"` + + // Dest upload: where the archive's one entry ends up, absolute or starting with ~/; whatever is there is replaced. Dest *string `json:"dest,omitempty"` Id string `json:"id"` Op ClientRequestOp `json:"op"` + // Path download, read_file, write_file: the file (or, for download, file or directory) on the client, absolute or starting with ~/. + Path *string `json:"path,omitempty"` + // TimeoutMs bash: kill the process group after this long. TimeoutMs *int `json:"timeout_ms,omitempty"` // Tool hands: the byre-hands tool to call. Tool *string `json:"tool,omitempty"` - // Transfer copy: the transfer id to download from /api/clients/transfers/{transferId}. + // Transfer upload: the transfer id to get from /api/clients/transfers/{transferId}. download: the transfer id to put the archive to. Transfer *string `json:"transfer,omitempty"` } @@ -1539,6 +1557,9 @@ type ServerInterface interface { // (GET /api/clients/transfers/{transferId}) GetClientTransfer(w http.ResponseWriter, r *http.Request, transferId string) + // (PUT /api/clients/transfers/{transferId}) + PutClientTransfer(w http.ResponseWriter, r *http.Request, transferId string) + // (GET /api/events) StreamEvents(w http.ResponseWriter, r *http.Request, params StreamEventsParams) @@ -1814,6 +1835,32 @@ func (siw *ServerInterfaceWrapper) GetClientTransfer(w http.ResponseWriter, r *h handler.ServeHTTP(w, r) } +// PutClientTransfer operation middleware +func (siw *ServerInterfaceWrapper) PutClientTransfer(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "transferId" ------------- + var transferId string + + err = runtime.BindStyledParameterWithOptions("simple", "transferId", r.PathValue("transferId"), &transferId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "transferId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PutClientTransfer(w, r, transferId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // StreamEvents operation middleware func (siw *ServerInterfaceWrapper) StreamEvents(w http.ResponseWriter, r *http.Request) { @@ -3640,6 +3687,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox/{itemId}", wrapper.GetInboxItem) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/clients/connect", wrapper.ConnectClient) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/clients/transfers/{transferId}", wrapper.GetClientTransfer) + m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/clients/transfers/{transferId}", wrapper.PutClientTransfer) 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) @@ -3656,242 +3704,245 @@ 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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", + "QYjy5dIsb9GMnQVmB/f5vQrIfPz4fwYA", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/clients/clients.go b/internal/clients/clients.go index 5138035..c8a2df9 100644 --- a/internal/clients/clients.go +++ b/internal/clients/clients.go @@ -32,8 +32,9 @@ type Client struct { } type Transfer struct { - Dir string - Name string + Dir string + Name string + Receive bool } type conn struct { diff --git a/internal/server/clients.go b/internal/server/clients.go index 94285f3..9fb4062 100644 --- a/internal/server/clients.go +++ b/internal/server/clients.go @@ -4,9 +4,12 @@ import ( "bytes" "errors" "fmt" + "io" "log/slog" "net/http" + "os" "os/exec" + "path/filepath" "time" "github.com/coder/websocket" @@ -38,8 +41,8 @@ func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) { func (s *Server) GetClientTransfer(w http.ResponseWriter, r *http.Request, transferID string) { t, ok := s.clients.Transfer(transferID) - if !ok { - writeError(w, http.StatusNotFound, fmt.Errorf("no transfer %s", transferID)) + if !ok || t.Receive { + writeError(w, http.StatusNotFound, fmt.Errorf("no transfer %s to send", transferID)) return } @@ -54,3 +57,27 @@ func (s *Server) GetClientTransfer(w http.ResponseWriter, r *http.Request, trans panic(http.ErrAbortHandler) } } + +func (s *Server) PutClientTransfer(w http.ResponseWriter, r *http.Request, transferID string) { + t, ok := s.clients.Transfer(transferID) + if !ok || !t.Receive { + writeError(w, http.StatusNotFound, fmt.Errorf("no transfer %s to receive", transferID)) + return + } + + f, err := os.OpenFile(filepath.Join(t.Dir, t.Name), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err != nil { + writeError(w, http.StatusNotFound, fmt.Errorf("transfer %s: %w", transferID, err)) + return + } + _, err = io.Copy(f, r.Body) + cerr := f.Close() + if err == nil { + err = cerr + } + if err != nil { + writeError(w, http.StatusInternalServerError, fmt.Errorf("receive transfer %s: %w", transferID, err)) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/mac/Sources/Byre/Remote.swift b/mac/Sources/Byre/Remote.swift index 87bb32a..5e9f387 100644 --- a/mac/Sources/Byre/Remote.swift +++ b/mac/Sources/Byre/Remote.swift @@ -8,6 +8,8 @@ typealias ClientReply = Components.Schemas.ClientReply private let retryAfter = Duration.seconds(2) private let maxOutput = 64 << 10 +private let maxMessage = 256 << 20 +private let maxRead = 64 << 20 private let copyTimeoutMs = 10 * 60 * 1000 @MainActor @@ -26,6 +28,7 @@ final class Remote { return } let task = api.session.webSocketTask(with: api.url("/api/clients/connect", scheme: "wss")) + task.maximumMessageSize = maxMessage ws = task task.resume() receive(task) @@ -93,8 +96,24 @@ final class Remote { let home = FileManager.default.homeDirectoryForCurrentUser.path let (code, output) = try await spawn(["/bin/zsh", "-l", "-c", req.command ?? ""], in: home, timeoutMs: req.timeoutMs ?? 120_000) return ClientReply(id: req.id, exitCode: code, output: output) - case .copy: - try await copy(transfer: req.transfer ?? "", to: req.dest ?? "") + case .upload: + try await upload(transfer: req.transfer ?? "", to: req.dest ?? "") + return ClientReply(id: req.id) + case .download: + try await download(req.path ?? "", transfer: req.transfer ?? "") + return ClientReply(id: req.id) + case .readFile: + let path = req.path ?? "" + let data = try await Task.detached { + try readFile(path) + }.value + return ClientReply(id: req.id, data: Base64EncodedData(data)) + case .writeFile: + let path = req.path ?? "" + let data = Data(req.data?.data ?? []) + try await Task.detached { + try writeFile(path, data) + }.value return ClientReply(id: req.id) case .hands: let result = try await hands.call(req.tool ?? "", req.arguments?.additionalProperties ?? OpenAPIObjectContainer()) @@ -102,8 +121,30 @@ final class Remote { } } - private func copy(transfer: String, to dest: String) async throws { - let target = URL(filePath: (dest as NSString).expandingTildeInPath) + private func download(_ path: String, transfer: String) async throws { + let source = expand(path) + let archive = FileManager.default.temporaryDirectory.appending(path: "byre-download-\(UUID().uuidString).tgz") + defer { + try? FileManager.default.removeItem(at: archive) + } + let parent = source.deletingLastPathComponent().path + let (status, output) = try await spawn(["/usr/bin/tar", "-czf", archive.path, "-C", parent, "--", source.lastPathComponent], in: parent, timeoutMs: copyTimeoutMs) + if status != 0 { + throw ApiError(status: 0, message: "pack: tar exited \(status): \(output)") + } + + var request = URLRequest(url: api.url("/api/clients/transfers/\(transfer)")) + request.httpMethod = "PUT" + request.setValue("application/gzip", forHTTPHeaderField: "Content-Type") + let (body, response) = try await api.session.upload(for: request, fromFile: archive) + let code = (response as! HTTPURLResponse).statusCode + if code != 204 { + throw ApiError(status: code, message: "upload transfer: \(code) \(String(decoding: body, as: UTF8.self))") + } + } + + private func upload(transfer: String, to dest: String) async throws { + let target = expand(dest) let parent = target.deletingLastPathComponent() let fm = FileManager.default try fm.createDirectory(at: parent, withIntermediateDirectories: true) @@ -117,7 +158,7 @@ final class Remote { throw ApiError(status: code, message: "download transfer: \(code) \(String(decoding: (try? Data(contentsOf: archive)) ?? Data(), as: UTF8.self))") } - let tmp = parent.appending(path: ".byre-copy-\(UUID().uuidString)", directoryHint: .isDirectory) + let tmp = parent.appending(path: ".byre-upload-\(UUID().uuidString)", directoryHint: .isDirectory) try fm.createDirectory(at: tmp, withIntermediateDirectories: false) defer { try? fm.removeItem(at: tmp) @@ -137,6 +178,37 @@ final class Remote { } } +private func expand(_ path: String) -> URL { + URL(filePath: (path as NSString).expandingTildeInPath) +} + +private func readFile(_ path: String) throws -> Data { + let source = expand(path).resolvingSymlinksInPath() + let type = try FileManager.default.attributesOfItem(atPath: source.path)[.type] as? FileAttributeType + guard type == .typeRegular else { + throw ApiError(status: 0, message: "\(path) isn't a regular file") + } + let h = try FileHandle(forReadingFrom: source) + defer { + try? h.close() + } + let data = try h.read(upToCount: maxRead + 1) ?? Data() + if data.count > maxRead { + throw ApiError(status: 0, message: "\(path) is over the \(maxRead >> 20) MiB read limit; use client_download") + } + return data +} + +private func writeFile(_ path: String, _ data: Data) throws { + let target = expand(path).resolvingSymlinksInPath() + let fm = FileManager.default + if let attrs = try? fm.attributesOfItem(atPath: target.path), attrs[.type] as? FileAttributeType != .typeRegular { + throw ApiError(status: 0, message: "\(path) isn't a regular file") + } + try fm.createDirectory(at: target.deletingLastPathComponent(), withIntermediateDirectories: true) + try data.write(to: target) +} + private final class Flag: @unchecked Sendable { private let lock = NSLock() private var value = false