From fab6ce583196304cb063bd89e14ac0ce80983e90 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 21 Oct 2026 10:48:59 -0700 Subject: [PATCH] client-exec CLI --- api/openapi.yaml | 58 ++- cmd/byre/clientexec.go | 88 ++++ cmd/byre/main.go | 6 +- cmd/byre/screen.go | 39 ++ cmd/byre/serve.go | 7 +- docs/operations.md | 2 + go.mod | 1 + go.sum | 2 + internal/agents/clients.go | 12 +- internal/agents/kb/client-exec.md | 37 ++ internal/agents/sign.go | 2 +- internal/api/api.gen.go | 698 ++++++++++++++++-------------- internal/clients/clients.go | 49 ++- internal/server/clients.go | 54 +++ internal/server/server.go | 13 +- mac/Sources/Byre/Remote.swift | 56 ++- 16 files changed, 760 insertions(+), 364 deletions(-) create mode 100644 cmd/byre/clientexec.go create mode 100644 cmd/byre/screen.go create mode 100644 internal/agents/kb/client-exec.md diff --git a/api/openapi.yaml b/api/openapi.yaml index 89fde0d..e4235c2 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1266,8 +1266,8 @@ paths: get: operationId: getTailnet description: > - The state of byre's own tailnet node. It is the only request byre answers on its loopback address, where it - needs no identity, so the app on the same Mac can sign the node in before it is reachable on the tailnet. + The state of byre's own tailnet node. byre answers it on its loopback address, where it needs no identity, so + the app on the same Mac can sign the node in before it is reachable on the tailnet. responses: "200": description: The node's state. @@ -1275,6 +1275,29 @@ paths: application/json: schema: { $ref: "#/components/schemas/Tailnet" } + /api/clients/exec: + post: + operationId: execClient + description: > + Runs a command on a client Mac as client_bash does, for `byre client-exec`. It needs the loopback token as a + bearer token, which byre puts in BYRE_TOKEN for everything it runs. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ClientExec" } + responses: + "200": + description: The command ran; exit_code is -1 when its timeout killed it. + content: + application/json: + schema: { $ref: "#/components/schemas/ClientExecResult" } + "503": + description: No such client is connected, or it didn't acknowledge the request within 2 seconds, or it disconnected before replying. + content: + application/json: + schema: { $ref: "#/components/schemas/Error" } + /api/admin/usage-commands: get: operationId: getUsageCommands @@ -2210,9 +2233,9 @@ components: description: Work a task agent, or a merge, asks a connected Mac to do, sent over /api/clients/connect. properties: id: { type: string } - op: { type: string, enum: [bash, upload, download, read_file, write_file, hands, sign] } - 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, sign: kill the process group after this long." } + op: { type: string, enum: [bash, exec, upload, download, read_file, write_file, hands, sign] } + command: { type: string, description: "bash, exec: run with zsh as a login shell in the home directory, in its own process group." } + timeout_ms: { type: integer, description: "bash, exec, sign: kill the process group after this long." } ident: { type: string, description: "sign: the committer, as \"Name \", to sign as when the Mac's git config has no user.signingkey." } 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." } @@ -2227,17 +2250,40 @@ components: ClientReply: type: object required: [id] + description: > + The app sends one reply with accepted set as soon as it receives a request, so byre knows within a moment that + the Mac is listening, then one final reply without it. properties: id: { type: string, description: The request's id. } + accepted: { type: boolean, description: "Set only on the first reply, which says the app received the request and started on it." } error: { type: string, description: Set when the request couldn't be carried out. } - exit_code: { type: integer, description: "bash, sign: the exit status, -1 when it was killed by its timeout." } + exit_code: { type: integer, description: "bash, exec, sign: 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. sign: the signing program's stderr, where it writes its GnuPG status lines, and the login shell's output." } + stdout: { type: string, format: byte, description: "exec: everything the command wrote to stdout." } + stderr: { type: string, format: byte, description: "exec: everything the command wrote to stderr." } data: { type: string, format: byte, description: "read_file: the file's contents. sign: the detached signature." } result: type: object additionalProperties: true description: "hands: the tool's MCP CallToolResult." + ClientExec: + type: object + required: [command, timeout_ms] + properties: + client: { type: string, description: "The client's name or id; absent for the most recently active one." } + command: { type: string, description: Run with zsh as a login shell in the home directory, in its own process group. } + timeout_ms: { type: integer, description: Kill the process group after this long. } + + ClientExecResult: + type: object + required: [client, exit_code, stdout, stderr] + properties: + client: { type: string, description: The id of the client that ran it. } + exit_code: { type: integer } + stdout: { type: string, format: byte } + stderr: { type: string, format: byte } + TurnRef: type: object required: [session_id, task_id, vendor] diff --git a/cmd/byre/clientexec.go b/cmd/byre/clientexec.go new file mode 100644 index 0000000..ad60816 --- /dev/null +++ b/cmd/byre/clientexec.go @@ -0,0 +1,88 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "strings" + "time" + + "g.fc.run/ai/byre/internal/api" +) + +const ( + execUnavailable = 75 + execTimedOut = 124 + execGrace = time.Minute +) + +func clientExecCmd(args []string) { + fs := flag.NewFlagSet("client-exec", flag.ExitOnError) + client := fs.String("client", "", "the client's name or id; the most recently active one when absent") + timeout := fs.Int("timeout", 30, "kill the command after this many seconds") + fs.Parse(args) + if fs.NArg() == 0 || *timeout <= 0 { + usage() + } + token := os.Getenv("BYRE_TOKEN") + if token == "" { + execFail(1, "BYRE_TOKEN is not set; byre sets it for everything it runs") + } + + words := make([]string, fs.NArg()) + for i, w := range fs.Args() { + words[i] = "'" + strings.ReplaceAll(w, "'", `'\''`) + "'" + } + req := api.ClientExec{Command: strings.Join(words, " "), TimeoutMs: *timeout * 1000} + if *client != "" { + req.Client = client + } + body, err := json.Marshal(req) + if err != nil { + execFail(1, err.Error()) + } + hr, err := http.NewRequest(http.MethodPost, loopbackURL+"/api/clients/exec", bytes.NewReader(body)) + if err != nil { + execFail(1, err.Error()) + } + hr.Header.Set("Authorization", "Bearer "+token) + hr.Header.Set("Content-Type", "application/json") + hc := &http.Client{Timeout: time.Duration(*timeout)*time.Second + execGrace} + resp, err := hc.Do(hr) + if err != nil { + execFail(execUnavailable, err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + e := api.Error{} + err = json.NewDecoder(resp.Body).Decode(&e) + if err != nil { + e.Message = resp.Status + } + code := 1 + if resp.StatusCode == http.StatusServiceUnavailable { + code = execUnavailable + } + execFail(code, e.Message) + } + res := api.ClientExecResult{} + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + execFail(1, fmt.Sprintf("decode reply: %s", err)) + } + os.Stdout.Write(res.Stdout) + os.Stderr.Write(res.Stderr) + if res.ExitCode == -1 { + execFail(execTimedOut, fmt.Sprintf("killed on %s after %ds", res.Client, *timeout)) + } + os.Exit(res.ExitCode) +} + +func execFail(code int, msg string) { + fmt.Fprintln(os.Stderr, "byre client-exec: "+msg) + os.Exit(code) +} diff --git a/cmd/byre/main.go b/cmd/byre/main.go index 46db25b..ce5dc10 100644 --- a/cmd/byre/main.go +++ b/cmd/byre/main.go @@ -26,13 +26,17 @@ func main() { grepCmd(os.Args[2:]) case "list": listCmd(os.Args[2:]) + case "client-exec": + clientExecCmd(os.Args[2:]) + case "screen": + screenCmd() default: usage() } } func usage() { - fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre run [--resume ID] | byre signing CHECKOUT | byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH | byre list PATH") + fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre run [--resume ID] | byre signing CHECKOUT | byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH | byre list PATH | byre client-exec [--client NAME] [--timeout SECS] -- CMD... | byre screen") os.Exit(2) } diff --git a/cmd/byre/screen.go b/cmd/byre/screen.go new file mode 100644 index 0000000..6430e4e --- /dev/null +++ b/cmd/byre/screen.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + + "howett.net/plist" +) + +type ioregRoot struct { + IOConsoleUsers []struct { + UID int `plist:"kCGSSessionUserIDKey"` + Locked bool `plist:"CGSSessionScreenIsLocked"` + } `plist:"IOConsoleUsers"` +} + +func screenCmd() { + out, err := exec.Command("ioreg", "-n", "Root", "-d1", "-a").Output() + if err != nil { + fatal("ioreg", "err", err) + } + root := ioregRoot{} + _, err = plist.Unmarshal(out, &root) + if err != nil { + fatal("decode ioreg", "err", err) + } + state := "no-session" + for _, u := range root.IOConsoleUsers { + if u.UID != os.Getuid() { + continue + } + state = "unlocked" + if u.Locked { + state = "locked" + } + } + fmt.Println(state) +} diff --git a/cmd/byre/serve.go b/cmd/byre/serve.go index 1b2eed6..38d3bfc 100644 --- a/cmd/byre/serve.go +++ b/cmd/byre/serve.go @@ -73,6 +73,9 @@ func serveCmd(args []string) { if err := os.WriteFile(filepath.Join(*dataDir, "loopback-token"), []byte(token), 0o600); err != nil { fatal("write loopback credential", "err", err) } + if err := os.Setenv("BYRE_TOKEN", token); err != nil { + fatal("export loopback credential", "err", err) + } home, err := os.UserHomeDir() if err != nil { fatal("home directory", "err", err) @@ -101,7 +104,7 @@ func serveCmd(args []string) { } defer ts.Close() - srv, err := server.New(*dataDir, *ports, ts) + srv, err := server.New(*dataDir, *ports, ts, token) if err != nil { fatal("start server", "err", err) } @@ -115,7 +118,7 @@ func serveCmd(args []string) { go updateDaily(ctx) base := func(net.Listener) context.Context { return ctx } - loopback := &http.Server{Addr: *listenAddr, Handler: srv.LoopbackHandler(token), BaseContext: base} + loopback := &http.Server{Addr: *listenAddr, Handler: srv.LoopbackHandler(), BaseContext: base} tailnet := &http.Server{Handler: srv.Handler(), BaseContext: base} go func() { diff --git a/docs/operations.md b/docs/operations.md index 7679d7a..0dd6586 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -105,6 +105,8 @@ byre never signs with a key on the machine it runs on. When a project checkout's - The app pings every 10s and reconnects after 25s with no pong. 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. - `log show --info --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` on a client shows `client request : ` lines and `byre-hands` starts and exits, which are info level and kept only in memory, so they cover the last few minutes at most; connection drops (`client connection closed`, `client connection got no pong`) are notices, kept on disk, so `log show --start` reaches much further back for them. byre's log shows `client connected` and `client disconnected`. +- The app acknowledges every request the moment it receives one; a client that hasn't within 2s fails the call, so a dead connection fails fast rather than at the request's own timeout. +- `byre client-exec` (how to use it, and `byre screen`, is the `client-exec` knowledge-base article) calls `POST /api/clients/exec` on the loopback address with the loopback token, which byre exports as `BYRE_TOKEN` to everything it starts. byre's log has a `client exec` line per call with the client, command, exit status and duration, never the output, since it may hold credentials. ## Screen and app control diff --git a/go.mod b/go.mod index c8a06f1..f6472ed 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20260915211658-a6f909f08a72 // indirect + howett.net/plist v1.0.0 // indirect modernc.org/libc v1.77.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect diff --git a/go.sum b/go.sum index 5b41bbf..79021be 100644 --- a/go.sum +++ b/go.sum @@ -166,6 +166,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jellydator/ttlcache/v3 v3.4.1 h1:bOdXmXiycyK6E6Qjyuj5vl+/vU3SCOoDs8a86NbHjAQ= github.com/jellydator/ttlcache/v3 v3.4.1/go.mod h1:j7LO12PNghFg5+0v9budMAT4rDK4JY969jb9vOdOBBk= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5GTEn90= github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -401,6 +402,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/agents/clients.go b/internal/agents/clients.go index 68ec331..b932aab 100644 --- a/internal/agents/clients.go +++ b/internal/agents/clients.go @@ -10,7 +10,6 @@ import ( "os" "os/exec" "path/filepath" - "sort" "strings" "time" @@ -220,17 +219,8 @@ func (m *Manager) clientTools(l *live) []mcp.Tool { } } -func (m *Manager) recentClients() []clients.Client { - rows := m.clients.List() - sort.SliceStable(rows, func(i, j int) bool { - a, b := rows[i].LastActionAt, rows[j].LastActionAt - return a != nil && (b == nil || a.After(*b)) - }) - return rows -} - func (m *Manager) listClients() string { - rows := m.recentClients() + rows := m.clients.Recent() if len(rows) == 0 { return "No client Macs are connected." } diff --git a/internal/agents/kb/client-exec.md b/internal/agents/kb/client-exec.md new file mode 100644 index 0000000..cbc8cfc --- /dev/null +++ b/internal/agents/kb/client-exec.md @@ -0,0 +1,37 @@ +How a script or plain shell runs a command on a client Mac without an agent (byre client-exec), and how to check either Mac's screen lock (byre screen). + +# Running commands on a client Mac from a shell + +`client_bash` needs an agent. For a script or plain shell on the machine byre runs on, such as a wrapper that fetches a token from the human's Mac, use: + +``` +byre client-exec [--client NAME] [--timeout SECS] -- CMD... +``` + +`byre` is `/Applications/Byre.app/Contents/Helpers/byre` on every Mac with the app. + +- It runs CMD as `client_bash` does: zsh as a login shell in the home directory, in the app's session, as the human. That session is what GUI-only commands need. +- Without `--client` it picks the most recently active client, the one `list_clients` lists first. +- Each argument reaches the client as one word, so shell syntax needs `-- zsh -c '...'`. +- The command's stdout and stderr come back separately and byte for byte, once it ends, written to client-exec's own stdout and stderr. +- Exit status: the command's own; 75 when no client is connected, it didn't acknowledge the request within 2s, or it disconnected; 124 when `--timeout` (default 30s) killed it. A one-line `byre client-exec: ...` on stderr says why. + +## Credentials + +It needs `BYRE_TOKEN`, which byre sets for everything it starts: agents, their tools and the processes those run, and the task's terminals. A process byre didn't start reads it from `loopback-token` in byre's data directory (`~/Library/Application Support/byre/` by default); it changes every time byre restarts, so read it at each call: + +``` +BYRE_TOKEN=$(<"$HOME/Library/Application Support/byre/loopback-token") byre client-exec -- ... +``` + +byre logs each call (client, command, exit status, duration), never its output. + +## Screen lock + +`byre screen` prints `unlocked`, `locked` or `no-session` (no GUI login for this user) for the Mac it runs on, and exits 0. Check the server Mac with `byre screen` and the client with: + +``` +byre client-exec -- /Applications/Byre.app/Contents/Helpers/byre screen +``` + +client-exec runs commands whatever the lock state; a script that needs an unlocked screen checks first and fails fast itself. diff --git a/internal/agents/sign.go b/internal/agents/sign.go index d5bb986..2120c56 100644 --- a/internal/agents/sign.go +++ b/internal/agents/sign.go @@ -15,7 +15,7 @@ const signTimeout = 2 * time.Minute func (m *Manager) signer(ctx context.Context, user api.User) func(payload []byte) ([]byte, error) { return func(payload []byte) ([]byte, error) { mine := []clients.Client{} - for _, c := range m.recentClients() { + for _, c := range m.clients.Recent() { if c.Login == user.Login { mine = append(mine, c) } diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index ff43872..0d438ac 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -142,6 +142,7 @@ func (e AttachmentKind) Valid() bool { const ( Bash ClientRequestOp = "bash" Download ClientRequestOp = "download" + Exec ClientRequestOp = "exec" Hands ClientRequestOp = "hands" ReadFile ClientRequestOp = "read_file" Sign ClientRequestOp = "sign" @@ -156,6 +157,8 @@ func (e ClientRequestOp) Valid() bool { return true case Download: return true + case Exec: + return true case Hands: return true case ReadFile: @@ -698,15 +701,39 @@ type BoardPostCreate struct { Text string `json:"text"` } -// ClientReply defines model for ClientReply. +// ClientExec defines model for ClientExec. +type ClientExec struct { + // Client The client's name or id; absent for the most recently active one. + Client *string `json:"client,omitempty"` + + // Command Run with zsh as a login shell in the home directory + Command string `json:"command"` + + // TimeoutMs Kill the process group after this long. + TimeoutMs int `json:"timeout_ms"` +} + +// ClientExecResult defines model for ClientExecResult. +type ClientExecResult struct { + // Client The id of the client that ran it. + Client string `json:"client"` + ExitCode int `json:"exit_code"` + Stderr []byte `json:"stderr"` + Stdout []byte `json:"stdout"` +} + +// ClientReply The app sends one reply with accepted set as soon as it receives a request, so byre knows within a moment that the Mac is listening, then one final reply without it. type ClientReply struct { + // Accepted Set only on the first reply, which says the app received the request and started on it. + Accepted *bool `json:"accepted,omitempty"` + // Data read_file: the file's contents. sign: the detached signature. Data *[]byte `json:"data,omitempty"` // Error Set when the request couldn't be carried out. Error *string `json:"error,omitempty"` - // ExitCode bash, sign: the exit status, -1 when it was killed by its timeout. + // ExitCode bash, exec, sign: the exit status, -1 when it was killed by its timeout. ExitCode *int `json:"exit_code,omitempty"` // Id The request's id. @@ -717,6 +744,12 @@ type ClientReply struct { // Result hands: the tool's MCP CallToolResult. Result *map[string]interface{} `json:"result,omitempty"` + + // Stderr exec: everything the command wrote to stderr. + Stderr *[]byte `json:"stderr,omitempty"` + + // Stdout exec: everything the command wrote to stdout. + Stdout *[]byte `json:"stdout,omitempty"` } // ClientRequest Work a task agent, or a merge, asks a connected Mac to do, sent over /api/clients/connect. @@ -724,7 +757,7 @@ type ClientRequest struct { // Arguments hands: the tool's arguments. Arguments *map[string]interface{} `json:"arguments,omitempty"` - // Command bash: run with zsh as a login shell in the home directory, in its own process group. + // Command bash, exec: run with zsh as a login shell in the home directory, in its own process group. Command *string `json:"command,omitempty"` // Data write_file: the file's new contents; parent directories are created. sign: the unsigned commit object, which the Mac signs with the program its git config names for its gpg.format (openpgp or x509), called as git calls it. @@ -741,7 +774,7 @@ type ClientRequest struct { // 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, sign: kill the process group after this long. + // TimeoutMs bash, exec, sign: kill the process group after this long. TimeoutMs *int `json:"timeout_ms,omitempty"` // Tool hands: the byre-hands tool to call. @@ -1744,6 +1777,9 @@ type PutWorkflowParams struct { // UpdateUsageCommandsJSONRequestBody defines body for UpdateUsageCommands for application/json ContentType. type UpdateUsageCommandsJSONRequestBody = UsageCommands +// ExecClientJSONRequestBody defines body for ExecClient for application/json ContentType. +type ExecClientJSONRequestBody = ClientExec + // SetClientSyncJSONRequestBody defines body for SetClientSync for application/json ContentType. type SetClientSyncJSONRequestBody = ClientSync @@ -1846,6 +1882,9 @@ type ServerInterface interface { // (GET /api/clients/connect) ConnectClient(w http.ResponseWriter, r *http.Request) + // (POST /api/clients/exec) + ExecClient(w http.ResponseWriter, r *http.Request) + // (PUT /api/clients/sync) SetClientSync(w http.ResponseWriter, r *http.Request) @@ -2191,6 +2230,20 @@ func (siw *ServerInterfaceWrapper) ConnectClient(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } +// ExecClient operation middleware +func (siw *ServerInterfaceWrapper) ExecClient(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ExecClient(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // SetClientSync operation middleware func (siw *ServerInterfaceWrapper) SetClientSync(w http.ResponseWriter, r *http.Request) { @@ -4455,6 +4508,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/events", wrapper.StreamEvents) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/admin/status", wrapper.GetStatus) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tailnet", wrapper.GetTailnet) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/clients/exec", wrapper.ExecClient) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/admin/usage-commands", wrapper.GetUsageCommands) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/admin/usage-commands", wrapper.UpdateUsageCommands) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/admin/permissions", wrapper.CheckPermissions) @@ -4471,320 +4525,328 @@ 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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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/clients/clients.go b/internal/clients/clients.go index 3e61079..a989c52 100644 --- a/internal/clients/clients.go +++ b/internal/clients/clients.go @@ -23,8 +23,11 @@ const ( pingInterval = 30 * time.Second pingTimeout = 15 * time.Second expireAfter = 24 * time.Hour + ackTimeout = 2 * time.Second ) +var ErrUnavailable = errors.New("client unavailable") + type Client struct { ID string Login string @@ -150,9 +153,12 @@ func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, hos ws.Close(websocket.StatusUnsupportedData, "messages must be ClientReply") return } + accepted := reply.Accepted != nil && *reply.Accepted c.mu.Lock() ch := c.pending[reply.Id] - delete(c.pending, reply.Id) + if !accepted { + delete(c.pending, reply.Id) + } c.mu.Unlock() if ch == nil { slog.Warn("client reply for no request", "host", host, "id", reply.Id) @@ -204,6 +210,15 @@ func (r *Registry) List() []Client { return out } +func (r *Registry) Recent() []Client { + rows := r.List() + sort.SliceStable(rows, func(i, j int) bool { + a, b := rows[i].LastActionAt, rows[j].LastActionAt + return a != nil && (b == nil || a.After(*b)) + }) + return rows +} + func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest) (api.ClientReply, error) { r.mu.Lock() matches := []entry{} @@ -220,7 +235,7 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest) r.mu.Unlock() slices.Sort(connected) if len(matches) == 0 { - return api.ClientReply{}, fmt.Errorf("no client named %q is connected; connected: %s", name, strings.Join(connected, ", ")) + return api.ClientReply{}, fmt.Errorf("%w: no client named %q is connected; connected: %s", ErrUnavailable, name, strings.Join(connected, ", ")) } if len(matches) > 1 { matched := make([]string, len(matches)) @@ -233,7 +248,7 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest) c := matches[0].conn req.Id = ids.New(ids.ClientRequest) - ch := make(chan api.ClientReply, 1) + ch := make(chan api.ClientReply, 2) c.mu.Lock() c.pending[req.Id] = ch c.mu.Unlock() @@ -252,16 +267,26 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest) return api.ClientReply{}, fmt.Errorf("send to %s: %w", name, err) } - select { - case reply := <-ch: - if reply.Error != nil { - return reply, errors.New(*reply.Error) + ack := time.NewTimer(ackTimeout) + defer ack.Stop() + for { + select { + case reply := <-ch: + if reply.Accepted != nil && *reply.Accepted { + ack.Stop() + continue + } + if reply.Error != nil { + return reply, errors.New(*reply.Error) + } + return reply, nil + case <-ack.C: + return api.ClientReply{}, fmt.Errorf("%w: %s didn't acknowledge the request within %s", ErrUnavailable, name, ackTimeout) + case <-c.done: + return api.ClientReply{}, fmt.Errorf("%w: %s disconnected before replying", ErrUnavailable, name) + case <-ctx.Done(): + return api.ClientReply{}, fmt.Errorf("%s didn't reply: %w", name, ctx.Err()) } - return reply, nil - case <-c.done: - return api.ClientReply{}, fmt.Errorf("%s disconnected before replying", name) - case <-ctx.Done(): - return api.ClientReply{}, fmt.Errorf("%s didn't reply: %w", name, ctx.Err()) } } diff --git a/internal/server/clients.go b/internal/server/clients.go index f2b0e37..1bb6a28 100644 --- a/internal/server/clients.go +++ b/internal/server/clients.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -11,10 +12,12 @@ import ( "os" "os/exec" "path/filepath" + "time" "github.com/coder/websocket" "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/clients" ) func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) { @@ -60,6 +63,57 @@ func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +const execGrace = 30 * time.Second + +func (s *Server) ExecClient(w http.ResponseWriter, r *http.Request) { + if !s.bearer(r) { + writeError(w, http.StatusUnauthorized, errors.New("exec needs the loopback token as a bearer token")) + return + } + req := api.ClientExec{} + if !readJSON(w, r, &req) { + return + } + if req.TimeoutMs <= 0 { + writeError(w, http.StatusBadRequest, errors.New("timeout_ms must be positive")) + return + } + + name := "" + if req.Client != nil { + name = *req.Client + } + if name == "" { + recent := s.clients.Recent() + if len(recent) == 0 { + writeError(w, http.StatusServiceUnavailable, errors.New("no client Macs are connected")) + return + } + name = recent[0].ID + } + + timeout := time.Duration(req.TimeoutMs) * time.Millisecond + ctx, cancel := context.WithTimeout(r.Context(), timeout+execGrace) + defer cancel() + start := time.Now() + reply, err := s.clients.Call(ctx, name, api.ClientRequest{Op: api.Exec, Command: &req.Command, TimeoutMs: &req.TimeoutMs}) + if err != nil { + slog.Info("client exec", "client", name, "command", req.Command, "err", err, "duration", time.Since(start)) + status := http.StatusInternalServerError + if errors.Is(err, clients.ErrUnavailable) { + status = http.StatusServiceUnavailable + } + writeError(w, status, err) + return + } + if reply.ExitCode == nil || reply.Stdout == nil || reply.Stderr == nil { + writeError(w, http.StatusInternalServerError, fmt.Errorf("%s's reply has no exit_code, stdout or stderr", name)) + return + } + slog.Info("client exec", "client", name, "command", req.Command, "exit", *reply.ExitCode, "duration", time.Since(start)) + writeJSON(w, http.StatusOK, api.ClientExecResult{Client: name, ExitCode: *reply.ExitCode, Stdout: *reply.Stdout, Stderr: *reply.Stderr}) +} + func (s *Server) GetClientTransfer(w http.ResponseWriter, r *http.Request, transferID string) { t, ok := s.clients.Transfer(transferID) if !ok || t.Receive { diff --git a/internal/server/server.go b/internal/server/server.go index e70e13f..c864db9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -35,6 +35,7 @@ type Server struct { terminals *terminal.Terminals tailnet *local.Client hostname string + token string startedAt time.Time usersMu sync.Mutex @@ -43,7 +44,7 @@ type Server struct { type ctxKey struct{} -func New(dataDir string, portFirst int, ts *tsnet.Server) (*Server, error) { +func New(dataDir string, portFirst int, ts *tsnet.Server, token string) (*Server, error) { err := os.MkdirAll(dataDir, 0o700) if err != nil { return nil, err @@ -62,6 +63,7 @@ func New(dataDir string, portFirst int, ts *tsnet.Server) (*Server, error) { hub: newHub(), tailnet: lc, hostname: ts.Hostname, + token: token, startedAt: time.Now().UTC(), users: map[string]string{}, } @@ -130,11 +132,12 @@ func (s *Server) Handler() http.Handler { return http.NewCrossOriginProtection().Handler(mux) } -func (s *Server) LoopbackHandler(token string) http.Handler { +func (s *Server) LoopbackHandler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /api/tailnet", s.GetTailnet) + mux.HandleFunc("POST /api/clients/exec", s.ExecClient) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+token)) != 1 { + if !s.bearer(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } @@ -143,6 +146,10 @@ func (s *Server) LoopbackHandler(token string) http.Handler { }) } +func (s *Server) bearer(r *http.Request) bool { + return subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+s.token)) == 1 +} + func (s *Server) GetTailnet(w http.ResponseWriter, r *http.Request) { st, err := s.tailnet.StatusWithoutPeers(r.Context()) if err != nil { diff --git a/mac/Sources/Byre/Remote.swift b/mac/Sources/Byre/Remote.swift index ec62722..5eed96a 100644 --- a/mac/Sources/Byre/Remote.swift +++ b/mac/Sources/Byre/Remote.swift @@ -134,6 +134,11 @@ final class Remote { } log.info("client request \(req.id, privacy: .public): \(req.op.rawValue, privacy: .public)") Task { + do { + try await task.send(.string(String(decoding: try JSONEncoder().encode(ClientReply(id: req.id, accepted: true)), as: UTF8.self))) + } catch { + log.error("acknowledge client request \(req.id, privacy: .public): \(errorText(error), privacy: .public)") + } var reply: ClientReply do { reply = try await perform(req) @@ -154,6 +159,10 @@ 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 .exec: + let home = FileManager.default.homeDirectoryForCurrentUser.path + let (code, stdout, stderr) = try await spawnSplit(["/bin/zsh", "-l", "-c", req.command ?? ""], in: home, timeoutMs: req.timeoutMs ?? 120_000) + return ClientReply(id: req.id, exitCode: code, stdout: Base64EncodedData(stdout), stderr: Base64EncodedData(stderr)) case .upload: try await upload(transfer: req.transfer ?? "", to: req.dest ?? "") return ClientReply(id: req.id) @@ -299,24 +308,52 @@ private func start(_ p: Process) throws -> AsyncStream { } private func spawn(_ argv: [String], in dir: String, timeoutMs: Int, input: FileHandle = .nullDevice) async throws -> (Int, String) { - let fm = FileManager.default - let out = fm.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log") + let out = try sinkFile() defer { - try? fm.removeItem(at: out) + try? FileManager.default.removeItem(at: out) } - fm.createFile(atPath: out.path, contents: nil, attributes: [.posixPermissions: 0o600]) let sink = try FileHandle(forWritingTo: out) defer { try? sink.close() } + let code = try await run(argv, in: dir, timeoutMs: timeoutMs, input: input, stdout: sink, stderr: sink) + return (code, tail(out)) +} +private func spawnSplit(_ argv: [String], in dir: String, timeoutMs: Int) async throws -> (Int, Data, Data) { + let fm = FileManager.default + let out = try sinkFile() + let err = try sinkFile() + defer { + try? fm.removeItem(at: out) + try? fm.removeItem(at: err) + } + let outSink = try FileHandle(forWritingTo: out) + let errSink = try FileHandle(forWritingTo: err) + defer { + try? outSink.close() + try? errSink.close() + } + let code = try await run(argv, in: dir, timeoutMs: timeoutMs, input: .nullDevice, stdout: outSink, stderr: errSink) + return (code, try Data(contentsOf: out), try Data(contentsOf: err)) +} + +private func sinkFile() throws -> URL { + let url = FileManager.default.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log") + guard FileManager.default.createFile(atPath: url.path, contents: nil, attributes: [.posixPermissions: 0o600]) else { + throw ApiError(status: 0, message: "create \(url.path)") + } + return url +} + +private func run(_ argv: [String], in dir: String, timeoutMs: Int, input: FileHandle, stdout: FileHandle, stderr: FileHandle) async throws -> Int { let p = Process() p.executableURL = URL(filePath: argv[0]) p.arguments = Array(argv.dropFirst()) p.currentDirectoryURL = URL(filePath: dir) p.standardInput = input - p.standardOutput = sink - p.standardError = sink + p.standardOutput = stdout + p.standardError = stderr let exit: AsyncStream do { exit = try start(p) @@ -332,14 +369,13 @@ private func spawn(_ argv: [String], in dir: String, timeoutMs: Int, input: File timer.cancel() let timedOut = (try? await timer.value) != nil - let output = tail(out) if timedOut { - return (-1, output) + return -1 } if p.terminationReason == .exit { - return (Int(p.terminationStatus), output) + return Int(p.terminationStatus) } - return (128 + Int(p.terminationStatus), output) + return 128 + Int(p.terminationStatus) } private func tail(_ file: URL) -> String {