Let task agents use client Macs directly: bash, copy and hands
This commit is contained in:
@@ -1,191 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
bundleTimeout = 5 * time.Minute
|
||||
MaxRunOutput = 64 << 10
|
||||
)
|
||||
|
||||
type runOnClientArgs struct {
|
||||
Paths []string `json:"paths"`
|
||||
Command string `json:"command"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
func (m *Manager) runOnClientTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "run_on_client",
|
||||
Description: "Ask the human to run a command on their own Mac, with files from your worktree, for example to install and launch an app you built. " +
|
||||
"byre packs paths into a bundle now, so later changes to them aren't sent, and files a card in the human's inbox showing why, the command and the files. " +
|
||||
"Every Mac the human has byre open on shows the card with Run and Dismiss; each Mac where they click Run downloads the bundle, unpacks it into a new empty directory, " +
|
||||
"and runs command there with zsh as a login shell, as the human. The directory is deleted once the command finishes, so copy anything that must stay elsewhere first. " +
|
||||
"The command keeps running if it quits byre's app, and the app reports it when it next starts. " +
|
||||
"This call returns at once; you don't need to end your turn. Each Mac's run reaches you as a message with its host name, exit status and the last 64 KiB of its output. " +
|
||||
"The card stays until the human dismisses it, so it can be run again or on another Mac.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"paths": {"type": "array", "items": {"type": "string"}, "description": "Files or directories to send, relative to your worktree; each is unpacked at the same relative path. May be empty."},
|
||||
"command": {"type": "string", "description": "The zsh command to run in the directory the bundle unpacks into, e.g. 'rm -rf ~/Applications/byre.app && ditto mac/.build/byre.app ~/Applications/byre.app && (pkill -x Byre; open ~/Applications/byre.app)'."},
|
||||
"why": {"type": "string", "description": "One or two sentences for the human: what running it does and why."}
|
||||
},
|
||||
"required": ["paths", "command", "why"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.runOnClient(ctx, l, raw)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) runOnClient(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
args := runOnClientArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed run_on_client call: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(args.Command) == "" {
|
||||
return "", errors.New("command is empty")
|
||||
}
|
||||
if strings.TrimSpace(args.Why) == "" {
|
||||
return "", errors.New("why is empty: say what running it does and why")
|
||||
}
|
||||
|
||||
paths, err := checkBundlePaths(l.dir, args.Paths)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id := store.NewID()
|
||||
size, err := m.writeBundle(ctx, l.dir, paths, m.store.BundlePath(id))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"why": args.Why,
|
||||
"command": args.Command,
|
||||
"paths": paths,
|
||||
"bytes": size,
|
||||
}
|
||||
err = m.store.CreateInboxItem(ctx, m.newItem(l, id, id, api.ClientRun, payload))
|
||||
if err != nil {
|
||||
os.Remove(m.store.BundlePath(id))
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Run request %s filed in the human's inbox with a %d-byte bundle. Each Mac's run will reach you as a message.", id, size), nil
|
||||
}
|
||||
|
||||
func checkBundlePaths(dir string, paths []string) ([]string, error) {
|
||||
root, err := os.OpenRoot(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
out := []string{}
|
||||
for _, p := range paths {
|
||||
clean := filepath.Clean(p)
|
||||
if filepath.IsAbs(clean) || clean == "." || strings.HasPrefix(clean, "-") {
|
||||
return nil, fmt.Errorf("path %q must be relative to your worktree and inside it", p)
|
||||
}
|
||||
_, err = root.Lstat(clean)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("path %q: %w", p, err)
|
||||
}
|
||||
out = append(out, clean)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Manager) writeBundle(ctx context.Context, dir string, paths []string, dest string) (int64, error) {
|
||||
err := os.MkdirAll(filepath.Dir(dest), 0o755)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
tmp := dest + ".tmp"
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, bundleTimeout)
|
||||
defer cancel()
|
||||
// -T /dev/null lets tar write an empty archive when paths is empty.
|
||||
cmd := exec.CommandContext(cctx, "tar", append([]string{"-czf", tmp, "-C", dir, "-T", "/dev/null", "--"}, paths...)...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
os.Remove(tmp)
|
||||
return 0, fmt.Errorf("pack bundle: %w: %s", err, out)
|
||||
}
|
||||
|
||||
err = os.Rename(tmp, dest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
info, err := os.Stat(dest)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return info.Size(), nil
|
||||
}
|
||||
|
||||
func (m *Manager) ReportRun(ctx context.Context, user, itemID string, report api.RunReport) (api.InboxItem, error) {
|
||||
item, err := m.store.GetInboxItem(ctx, itemID)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if item.Kind != api.ClientRun {
|
||||
return item, fmt.Errorf("%w: item %s is a %s, not a client_run", ErrInvalid, itemID, item.Kind)
|
||||
}
|
||||
client := strings.TrimSpace(report.Client)
|
||||
if client == "" {
|
||||
return item, fmt.Errorf("%w: client is empty", ErrInvalid)
|
||||
}
|
||||
if strings.TrimSpace(report.RunId) == "" {
|
||||
return item, fmt.Errorf("%w: run_id is empty", ErrInvalid)
|
||||
}
|
||||
|
||||
output := report.Output
|
||||
if len(output) > MaxRunOutput {
|
||||
output = strings.ToValidUTF8(output[len(output)-MaxRunOutput:], "")
|
||||
}
|
||||
resp := api.InboxResponse{
|
||||
Id: report.RunId,
|
||||
ItemId: itemID,
|
||||
User: user,
|
||||
Type: api.Run,
|
||||
Text: &output,
|
||||
Client: &client,
|
||||
ExitCode: &report.ExitCode,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
added, err := m.store.AddRunResponse(ctx, resp)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
if !added {
|
||||
slog.Info("client run already reported", "item", itemID, "response", resp.Id, "client", client)
|
||||
return m.store.GetInboxItem(ctx, itemID)
|
||||
}
|
||||
slog.Info("client run reported", "item", itemID, "response", resp.Id, "user", user, "client", client, "exit", report.ExitCode)
|
||||
|
||||
go func() {
|
||||
err := m.Deliver(context.Background(), item.AgentSessionId)
|
||||
if err != nil {
|
||||
slog.Error("deliver run report", "session", item.AgentSessionId, "err", err)
|
||||
}
|
||||
}()
|
||||
return m.store.GetInboxItem(ctx, itemID)
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/clients"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBashTimeout = 2 * time.Minute
|
||||
maxBashTimeout = 10 * time.Minute
|
||||
replyGrace = 30 * time.Second
|
||||
copyTimeout = 10 * time.Minute
|
||||
handsTimeout = 11 * time.Minute
|
||||
)
|
||||
|
||||
func (m *Manager) clientTools(l *live) []mcp.Tool {
|
||||
return []mcp.Tool{
|
||||
{
|
||||
Name: "list_clients",
|
||||
Description: "List the Macs with byre's app open: the human's own machines, not the one byre (and your worktree) runs on. " +
|
||||
"Each has its name, which the other client tools take as client, the human using it, and when the human last did something in byre from it. " +
|
||||
"The most recently active one is listed first; it is usually the Mac the human sits at.",
|
||||
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.listClients(ctx)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_bash",
|
||||
Description: "Run a shell command on a client Mac, as the human, with zsh as a login shell in their home directory, and get its exit status and output (stdout and stderr together, the last 64 KiB). " +
|
||||
"It runs at once, with no approval. The command runs in its own process group, which is killed at the timeout. " +
|
||||
"A command that quits byre's app on that Mac, for example to install a new build, ends this call with the client disconnecting, but the command keeps running.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {"type": "string", "description": "The client's name from list_clients."},
|
||||
"command": {"type": "string"},
|
||||
"timeout_ms": {"type": "integer", "description": "Kill the command after this long (default 120000, at most 600000)."}
|
||||
},
|
||||
"required": ["client", "command"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.clientBash(ctx, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_copy",
|
||||
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(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {"type": "string", "description": "The client's name from list_clients."},
|
||||
"path": {"type": "string", "description": "The file or directory, relative to your worktree."},
|
||||
"dest": {"type": "string", "description": "Its new path on the client, absolute or starting with ~/, e.g. ~/Applications/byre.app."}
|
||||
},
|
||||
"required": ["client", "path", "dest"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.clientCopy(ctx, l, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_hands",
|
||||
Description: "Call one of your hands tools (screen_list, capture, ax_tree, act, browser) on a client Mac instead of the Mac byre runs on: " +
|
||||
"tool is the tool's name and arguments exactly what that tool takes. Window ids, refs and displays are that Mac's, so start with screen_list there. " +
|
||||
"Every agent using that Mac shares one set of refs, so read the tree again if another agent may have. " +
|
||||
"A call fails until the human has given byre's app Screen Recording and Accessibility (and Automation of Orion, for browser) on that Mac; say so when it does.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {"type": "string", "description": "The client's name from list_clients."},
|
||||
"tool": {"type": "string", "enum": ["screen_list", "capture", "ax_tree", "act", "browser"]},
|
||||
"arguments": {"type": "object", "description": "The tool's arguments."}
|
||||
},
|
||||
"required": ["client", "tool"]
|
||||
}`),
|
||||
Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
|
||||
return m.clientHands(ctx, raw)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) listClients(ctx context.Context) (string, error) {
|
||||
status, err := m.status(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type row struct {
|
||||
c clients.Client
|
||||
last *time.Time
|
||||
}
|
||||
rows := []row{}
|
||||
for _, c := range m.clients.List() {
|
||||
r := row{c: c}
|
||||
for _, s := range status.Clients {
|
||||
if s.Login == c.Login && s.Host == c.Host && s.Commit == c.Commit {
|
||||
r.last = s.LastActionAt
|
||||
}
|
||||
}
|
||||
rows = append(rows, r)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return "No client Macs are connected.", nil
|
||||
}
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
a, b := rows[i].last, rows[j].last
|
||||
return a != nil && (b == nil || a.After(*b))
|
||||
})
|
||||
|
||||
now := time.Now()
|
||||
b := &strings.Builder{}
|
||||
for i, r := range rows {
|
||||
fmt.Fprintf(b, "- %s (%s): ", r.c.Host, r.c.Login)
|
||||
if r.last == nil {
|
||||
b.WriteString("no action since byre started")
|
||||
} else {
|
||||
fmt.Fprintf(b, "last action %s ago", now.Sub(*r.last).Round(time.Second))
|
||||
}
|
||||
fmt.Fprintf(b, ", connected %s ago, app built from %s", now.Sub(r.c.ConnectedAt).Round(time.Second), r.c.Commit)
|
||||
if i == 0 && r.last != nil {
|
||||
b.WriteString(" (most recently active)")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
type clientBashArgs struct {
|
||||
Client string `json:"client"`
|
||||
Command string `json:"command"`
|
||||
TimeoutMs *int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
func (m *Manager) clientBash(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
args := clientBashArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed client_bash call: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(args.Command) == "" {
|
||||
return "", errors.New("command is empty")
|
||||
}
|
||||
timeout := defaultBashTimeout
|
||||
if args.TimeoutMs != nil {
|
||||
timeout = time.Duration(*args.TimeoutMs) * time.Millisecond
|
||||
}
|
||||
if timeout <= 0 || timeout > maxBashTimeout {
|
||||
return "", fmt.Errorf("timeout_ms must be between 1 and %d", maxBashTimeout.Milliseconds())
|
||||
}
|
||||
|
||||
ms := int(timeout.Milliseconds())
|
||||
cctx, cancel := context.WithTimeout(ctx, timeout+replyGrace)
|
||||
defer cancel()
|
||||
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Bash, Command: &args.Command, TimeoutMs: &ms})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if reply.ExitCode == nil || reply.Output == nil {
|
||||
return "", fmt.Errorf("%s's reply has no exit_code or output", args.Client)
|
||||
}
|
||||
status := fmt.Sprintf("exit status %d", *reply.ExitCode)
|
||||
if *reply.ExitCode == -1 {
|
||||
status = fmt.Sprintf("killed after %s", timeout)
|
||||
}
|
||||
return status + "\n" + *reply.Output, nil
|
||||
}
|
||||
|
||||
type clientCopyArgs 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{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed client_copy 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)
|
||||
}
|
||||
|
||||
clean := filepath.Clean(args.Path)
|
||||
if filepath.IsAbs(clean) {
|
||||
return "", fmt.Errorf("path %q must be relative to your worktree", args.Path)
|
||||
}
|
||||
root, err := os.OpenRoot(l.dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = root.Lstat(clean)
|
||||
root.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("path %q: %w", args.Path, err)
|
||||
}
|
||||
|
||||
full := filepath.Join(l.dir, clean)
|
||||
id := m.clients.AddTransfer(clients.Transfer{Dir: filepath.Dir(full), Name: filepath.Base(full)})
|
||||
defer m.clients.RemoveTransfer(id)
|
||||
|
||||
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})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil
|
||||
}
|
||||
|
||||
type clientHandsArgs struct {
|
||||
Client string `json:"client"`
|
||||
Tool string `json:"tool"`
|
||||
Arguments *map[string]any `json:"arguments"`
|
||||
}
|
||||
|
||||
func (m *Manager) clientHands(ctx context.Context, raw json.RawMessage) mcp.Result {
|
||||
args := clientHandsArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return mcp.TextResult(fmt.Sprintf("malformed client_hands call: %s", err), true)
|
||||
}
|
||||
if args.Arguments == nil {
|
||||
args.Arguments = &map[string]any{}
|
||||
}
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, handsTimeout)
|
||||
defer cancel()
|
||||
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Hands, Tool: &args.Tool, Arguments: args.Arguments})
|
||||
if err != nil {
|
||||
return mcp.TextResult(err.Error(), true)
|
||||
}
|
||||
if reply.Result == nil {
|
||||
return mcp.TextResult(fmt.Sprintf("%s's reply has no result", args.Client), true)
|
||||
}
|
||||
res := mcp.Result{}
|
||||
err = remarshal(*reply.Result, &res)
|
||||
if err != nil {
|
||||
return mcp.TextResult(fmt.Sprintf("%s's result isn't an MCP tool result: %s", args.Client, err), true)
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -492,16 +492,9 @@ func deliveryMessage(pending []store.Pending) string {
|
||||
fmt.Fprintf(b, "Your error %s: %s\n", p.Item.Id, p.Item.Payload["error"])
|
||||
case api.RateLimited:
|
||||
fmt.Fprintf(b, "Your rate-limited turn %s: %s\n", p.Item.Id, p.Item.Payload["error"])
|
||||
case api.ClientRun:
|
||||
fmt.Fprintf(b, "Your run request %s: %s\n", p.Item.Id, p.Item.Payload["command"])
|
||||
}
|
||||
|
||||
switch r.Type {
|
||||
case api.Run:
|
||||
fmt.Fprintf(b, "%s ran it on %s: exit status %d\n", r.User, *r.Client, *r.ExitCode)
|
||||
if r.Text != nil && *r.Text != "" {
|
||||
fmt.Fprintf(b, "Output:\n```\n%s\n```\n", strings.TrimRight(*r.Text, "\n"))
|
||||
}
|
||||
case api.Option:
|
||||
if p.Item.Payload["multiple"] == true {
|
||||
fmt.Fprintf(b, "%s checked:\n", r.User)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/claude"
|
||||
"g.fc.run/misc/byre/internal/clients"
|
||||
"g.fc.run/misc/byre/internal/git"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
@@ -39,6 +40,7 @@ type Manager struct {
|
||||
codexHome string
|
||||
status func(context.Context) (api.Status, error)
|
||||
addProject func(ctx context.Context, name, repoPath, createdBy string) (api.Project, error)
|
||||
clients *clients.Registry
|
||||
|
||||
mu sync.Mutex
|
||||
live map[string]*live
|
||||
@@ -117,7 +119,7 @@ func (l *live) implementer() bool {
|
||||
return l.owner == "" && l.reviewID == ""
|
||||
}
|
||||
|
||||
func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(context.Context) (api.Status, error), addProject func(ctx context.Context, name, repoPath, createdBy string) (api.Project, error)) *Manager {
|
||||
func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(context.Context) (api.Status, error), addProject func(ctx context.Context, name, repoPath, createdBy string) (api.Project, error), clientRegistry *clients.Registry) *Manager {
|
||||
m := &Manager{
|
||||
store: st,
|
||||
worktreeRoot: worktreeRoot,
|
||||
@@ -125,6 +127,7 @@ func New(st *store.Store, worktreeRoot, leadRoot, codexHome string, status func(
|
||||
codexHome: codexHome,
|
||||
status: status,
|
||||
addProject: addProject,
|
||||
clients: clientRegistry,
|
||||
live: map[string]*live{},
|
||||
starting: map[string]*start{},
|
||||
deliverLocks: map[string]*sessionLock{},
|
||||
@@ -709,7 +712,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.runOnClientTool(l), m.takeSnapshotTool(l)}, m.subtaskTools(l)...)
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l)}, append(m.subtaskTools(l), m.clientTools(l)...)...)
|
||||
if sess.Role == api.Implementer {
|
||||
g += longTitleGuidance(task)
|
||||
notes, err := m.notesText(ctx, task.CreatedBy, task.ProjectId)
|
||||
|
||||
@@ -113,10 +113,6 @@ func CheckResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxStat
|
||||
return "", fmt.Errorf("%w: item %s: "+format, append([]any{ErrInvalid, item.Id}, args...)...)
|
||||
}
|
||||
|
||||
if item.Kind == api.ClientRun && c.Type != api.Dismiss {
|
||||
return bad("a run request is only dismissed here; runs are reported to /api/inbox/%s/runs", item.Id)
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case api.Option:
|
||||
if !question {
|
||||
|
||||
+359
-329
@@ -135,6 +135,27 @@ func (e AttachmentKind) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for ClientRequestOp.
|
||||
const (
|
||||
Bash ClientRequestOp = "bash"
|
||||
Copy ClientRequestOp = "copy"
|
||||
Hands ClientRequestOp = "hands"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the ClientRequestOp enum.
|
||||
func (e ClientRequestOp) Valid() bool {
|
||||
switch e {
|
||||
case Bash:
|
||||
return true
|
||||
case Copy:
|
||||
return true
|
||||
case Hands:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for DirEntryType.
|
||||
const (
|
||||
DirEntryTypeDir DirEntryType = "dir"
|
||||
@@ -203,7 +224,6 @@ func (e FindingState) Valid() bool {
|
||||
|
||||
// Defines values for InboxItemKind.
|
||||
const (
|
||||
ClientRun InboxItemKind = "client_run"
|
||||
Question InboxItemKind = "question"
|
||||
RateLimited InboxItemKind = "rate_limited"
|
||||
SessionError InboxItemKind = "session_error"
|
||||
@@ -212,8 +232,6 @@ const (
|
||||
// Valid indicates whether the value is a known member of the InboxItemKind enum.
|
||||
func (e InboxItemKind) Valid() bool {
|
||||
switch e {
|
||||
case ClientRun:
|
||||
return true
|
||||
case Question:
|
||||
return true
|
||||
case RateLimited:
|
||||
@@ -230,7 +248,6 @@ const (
|
||||
Dismiss InboxResponseType = "dismiss"
|
||||
Option InboxResponseType = "option"
|
||||
Other InboxResponseType = "other"
|
||||
Run InboxResponseType = "run"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the InboxResponseType enum.
|
||||
@@ -242,8 +259,6 @@ func (e InboxResponseType) Valid() bool {
|
||||
return true
|
||||
case Other:
|
||||
return true
|
||||
case Run:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -637,6 +652,50 @@ type Blob struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// ClientReply defines model for ClientReply.
|
||||
type ClientReply struct {
|
||||
// Error Set when the request couldn't be carried out.
|
||||
Error *string `json:"error,omitempty"`
|
||||
|
||||
// ExitCode bash: the exit status, -1 when it was killed by its timeout.
|
||||
ExitCode *int `json:"exit_code,omitempty"`
|
||||
|
||||
// Id The request's id.
|
||||
Id string `json:"id"`
|
||||
|
||||
// Output bash: stdout and stderr together, at most their last 64 KiB.
|
||||
Output *string `json:"output,omitempty"`
|
||||
|
||||
// Result hands: the tool's MCP CallToolResult.
|
||||
Result *map[string]interface{} `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// ClientRequest Work a task agent asks a connected Mac to do, sent over /api/clients/connect.
|
||||
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 *string `json:"command,omitempty"`
|
||||
|
||||
// Dest copy: 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"`
|
||||
|
||||
// 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 *string `json:"transfer,omitempty"`
|
||||
}
|
||||
|
||||
// ClientRequestOp defines model for ClientRequest.Op.
|
||||
type ClientRequestOp string
|
||||
|
||||
// ConnectedClient defines model for ConnectedClient.
|
||||
type ConnectedClient struct {
|
||||
// Commit The git commit the app was built from
|
||||
@@ -771,7 +830,7 @@ type InboxItem struct {
|
||||
Id string `json:"id"`
|
||||
Kind InboxItemKind `json:"kind"`
|
||||
|
||||
// Payload question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error. client_run: why, command, paths (worktree paths in the bundle), bytes (the bundle's size).
|
||||
// Payload question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error.
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
ProjectId string `json:"project_id"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
@@ -793,25 +852,17 @@ type InboxRespond struct {
|
||||
// InboxResponse defines model for InboxResponse.
|
||||
type InboxResponse struct {
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
|
||||
// Client run: the host name of the Mac that ran the command.
|
||||
Client *string `json:"client,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
|
||||
|
||||
// ExitCode run: the command's exit status.
|
||||
ExitCode *int `json:"exit_code,omitempty"`
|
||||
Id string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeliveredAt *time.Time `json:"delivered_at,omitempty"`
|
||||
Id string `json:"id"`
|
||||
|
||||
// Images Blob ids sent to the agent as images alongside the response.
|
||||
Images []string `json:"images"`
|
||||
ItemId string `json:"item_id"`
|
||||
Options *[]string `json:"options,omitempty"`
|
||||
|
||||
// Text run: the command's output, at most its last 64 KiB.
|
||||
Text *string `json:"text,omitempty"`
|
||||
Type InboxResponseType `json:"type"`
|
||||
User string `json:"user"`
|
||||
Images []string `json:"images"`
|
||||
ItemId string `json:"item_id"`
|
||||
Options *[]string `json:"options,omitempty"`
|
||||
Text *string `json:"text,omitempty"`
|
||||
Type InboxResponseType `json:"type"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
// InboxResponseCreate defines model for InboxResponseCreate.
|
||||
@@ -975,16 +1026,6 @@ type Review struct {
|
||||
// ReviewState skipped: the snapshot's step changed no files, so no reviewer ran.
|
||||
type ReviewState string
|
||||
|
||||
// RunReport defines model for RunReport.
|
||||
type RunReport struct {
|
||||
Client string `json:"client"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Output string `json:"output"`
|
||||
|
||||
// RunId The Mac's id for this one run, used as the response's id; reporting it again records nothing new.
|
||||
RunId string `json:"run_id"`
|
||||
}
|
||||
|
||||
// SessionCreate defines model for SessionCreate.
|
||||
type SessionCreate struct {
|
||||
// Images Blob ids from uploadBlob, sent to the agent as images alongside the prompt.
|
||||
@@ -1441,9 +1482,6 @@ type GetTaskTerminalParams struct {
|
||||
// RespondInboxJSONRequestBody defines body for RespondInbox for application/json ContentType.
|
||||
type RespondInboxJSONRequestBody = InboxRespond
|
||||
|
||||
// ReportRunJSONRequestBody defines body for ReportRun for application/json ContentType.
|
||||
type ReportRunJSONRequestBody = RunReport
|
||||
|
||||
// UpdateMeJSONRequestBody defines body for UpdateMe for application/json ContentType.
|
||||
type UpdateMeJSONRequestBody = UserUpdate
|
||||
|
||||
@@ -1495,6 +1533,12 @@ type ServerInterface interface {
|
||||
// (GET /api/blobs/{blobId})
|
||||
GetBlob(w http.ResponseWriter, r *http.Request, blobId string)
|
||||
|
||||
// (GET /api/clients/connect)
|
||||
ConnectClient(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// (GET /api/clients/transfers/{transferId})
|
||||
GetClientTransfer(w http.ResponseWriter, r *http.Request, transferId string)
|
||||
|
||||
// (GET /api/events)
|
||||
StreamEvents(w http.ResponseWriter, r *http.Request, params StreamEventsParams)
|
||||
|
||||
@@ -1507,12 +1551,6 @@ type ServerInterface interface {
|
||||
// (GET /api/inbox/{itemId})
|
||||
GetInboxItem(w http.ResponseWriter, r *http.Request, itemId string)
|
||||
|
||||
// (GET /api/inbox/{itemId}/bundle)
|
||||
GetRunBundle(w http.ResponseWriter, r *http.Request, itemId string)
|
||||
|
||||
// (POST /api/inbox/{itemId}/runs)
|
||||
ReportRun(w http.ResponseWriter, r *http.Request, itemId string)
|
||||
|
||||
// (GET /api/me)
|
||||
GetMe(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
@@ -1736,6 +1774,46 @@ func (siw *ServerInterfaceWrapper) GetBlob(w http.ResponseWriter, r *http.Reques
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ConnectClient operation middleware
|
||||
func (siw *ServerInterfaceWrapper) ConnectClient(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.ConnectClient(w, r)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetClientTransfer operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetClientTransfer(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.GetClientTransfer(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) {
|
||||
|
||||
@@ -1915,58 +1993,6 @@ func (siw *ServerInterfaceWrapper) GetInboxItem(w http.ResponseWriter, r *http.R
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetRunBundle operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetRunBundle(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "itemId" -------------
|
||||
var itemId string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "itemId", r.PathValue("itemId"), &itemId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "itemId", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.GetRunBundle(w, r, itemId)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ReportRun operation middleware
|
||||
func (siw *ServerInterfaceWrapper) ReportRun(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "itemId" -------------
|
||||
var itemId string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "itemId", r.PathValue("itemId"), &itemId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "itemId", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.ReportRun(w, r, itemId)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetMe operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -3612,8 +3638,8 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/blobs/{blobId}", wrapper.GetBlob)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox", wrapper.ListInbox)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox/{itemId}", wrapper.GetInboxItem)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/inbox/{itemId}/bundle", wrapper.GetRunBundle)
|
||||
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/inbox/{itemId}/runs", wrapper.ReportRun)
|
||||
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.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)
|
||||
@@ -3630,238 +3656,242 @@ 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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",
|
||||
"IcrTpVneoBlbC8wO7vN7FZD59On/DgA=",
|
||||
"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",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
const (
|
||||
readLimit = 256 << 20
|
||||
pingInterval = 30 * time.Second
|
||||
pingTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Login string
|
||||
Host string
|
||||
Commit string
|
||||
ConnectedAt time.Time
|
||||
}
|
||||
|
||||
type Transfer struct {
|
||||
Dir string
|
||||
Name string
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
Client
|
||||
ws *websocket.Conn
|
||||
done chan struct{}
|
||||
mu sync.Mutex
|
||||
pending map[string]chan api.ClientReply
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
mu sync.Mutex
|
||||
conns map[string]*conn
|
||||
transfers map[string]Transfer
|
||||
}
|
||||
|
||||
func New() *Registry {
|
||||
return &Registry{conns: map[string]*conn{}, transfers: map[string]Transfer{}}
|
||||
}
|
||||
|
||||
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, client Client) {
|
||||
ws.SetReadLimit(readLimit)
|
||||
c := &conn{Client: client, ws: ws, done: make(chan struct{}), pending: map[string]chan api.ClientReply{}}
|
||||
|
||||
r.mu.Lock()
|
||||
old := r.conns[client.Host]
|
||||
r.conns[client.Host] = c
|
||||
r.mu.Unlock()
|
||||
if old != nil {
|
||||
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this host")
|
||||
}
|
||||
slog.Info("client connected", "host", client.Host, "login", client.Login, "commit", client.Commit)
|
||||
|
||||
defer func() {
|
||||
r.mu.Lock()
|
||||
if r.conns[client.Host] == c {
|
||||
delete(r.conns, client.Host)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
close(c.done)
|
||||
slog.Info("client disconnected", "host", client.Host, "login", client.Login)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go c.ping(ctx)
|
||||
|
||||
for {
|
||||
_, b, err := ws.Read(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
reply := api.ClientReply{}
|
||||
err = json.Unmarshal(b, &reply)
|
||||
if err != nil {
|
||||
ws.Close(websocket.StatusUnsupportedData, "messages must be ClientReply")
|
||||
return
|
||||
}
|
||||
c.mu.Lock()
|
||||
ch := c.pending[reply.Id]
|
||||
delete(c.pending, reply.Id)
|
||||
c.mu.Unlock()
|
||||
if ch == nil {
|
||||
slog.Warn("client reply for no request", "host", client.Host, "id", reply.Id)
|
||||
continue
|
||||
}
|
||||
ch <- reply
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) ping(ctx context.Context) {
|
||||
t := time.NewTicker(pingInterval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
pctx, cancel := context.WithTimeout(ctx, pingTimeout)
|
||||
err := c.ws.Ping(pctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
c.ws.Close(websocket.StatusGoingAway, "ping timed out")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) List() []Client {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := []Client{}
|
||||
for _, c := range r.conns {
|
||||
out = append(out, c.Client)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Registry) Call(ctx context.Context, host string, req api.ClientRequest) (api.ClientReply, error) {
|
||||
r.mu.Lock()
|
||||
c := r.conns[host]
|
||||
hosts := []string{}
|
||||
for h := range r.conns {
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
if c == nil {
|
||||
slices.Sort(hosts)
|
||||
return api.ClientReply{}, fmt.Errorf("no client named %q is connected; connected: %s", host, strings.Join(hosts, ", "))
|
||||
}
|
||||
|
||||
req.Id = rand.Text()
|
||||
ch := make(chan api.ClientReply, 1)
|
||||
c.mu.Lock()
|
||||
c.pending[req.Id] = ch
|
||||
c.mu.Unlock()
|
||||
defer func() {
|
||||
c.mu.Lock()
|
||||
delete(c.pending, req.Id)
|
||||
c.mu.Unlock()
|
||||
}()
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return api.ClientReply{}, err
|
||||
}
|
||||
err = c.ws.Write(ctx, websocket.MessageText, data)
|
||||
if err != nil {
|
||||
return api.ClientReply{}, fmt.Errorf("send to %s: %w", host, err)
|
||||
}
|
||||
|
||||
select {
|
||||
case reply := <-ch:
|
||||
if reply.Error != nil {
|
||||
return reply, errors.New(*reply.Error)
|
||||
}
|
||||
return reply, nil
|
||||
case <-c.done:
|
||||
return api.ClientReply{}, fmt.Errorf("%s disconnected before replying", host)
|
||||
case <-ctx.Done():
|
||||
return api.ClientReply{}, fmt.Errorf("%s didn't reply: %w", host, ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) AddTransfer(t Transfer) string {
|
||||
id := rand.Text()
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.transfers[id] = t
|
||||
return id
|
||||
}
|
||||
|
||||
func (r *Registry) RemoveTransfer(id string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.transfers, id)
|
||||
}
|
||||
|
||||
func (r *Registry) Transfer(id string) (Transfer, bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
t, ok := r.transfers[id]
|
||||
return t, ok
|
||||
}
|
||||
@@ -744,11 +744,16 @@ func (s *Session) callTool(raw json.RawMessage) (any, error) {
|
||||
return nil, fmt.Errorf("unknown tool %s in %s", p.Tool, p.Namespace)
|
||||
}
|
||||
|
||||
text, err := t.Handler(context.Background(), p.Arguments)
|
||||
if err != nil {
|
||||
return DynamicToolCallResponse{ContentItems: []DynamicToolCallOutputContentItem{{Type: DynamicToolCallOutputContentItemTypeInputText, Text: err.Error()}}}, nil
|
||||
res := t.Call(context.Background(), p.Arguments)
|
||||
items := []DynamicToolCallOutputContentItem{}
|
||||
for _, c := range res.Content {
|
||||
if c.Type == "image" {
|
||||
items = append(items, DynamicToolCallOutputContentItem{Type: DynamicToolCallOutputContentItemTypeInputImage, ImageURL: "data:" + c.MimeType + ";base64," + c.Data})
|
||||
continue
|
||||
}
|
||||
items = append(items, DynamicToolCallOutputContentItem{Type: DynamicToolCallOutputContentItemTypeInputText, Text: c.Text})
|
||||
}
|
||||
return DynamicToolCallResponse{Success: true, ContentItems: []DynamicToolCallOutputContentItem{{Type: DynamicToolCallOutputContentItemTypeInputText, Text: text}}}, nil
|
||||
return DynamicToolCallResponse{Success: !res.IsError, ContentItems: items}, nil
|
||||
}
|
||||
|
||||
func (s *Session) badLine(line []byte, reason string) {
|
||||
|
||||
+36
-16
@@ -13,6 +13,41 @@ type Tool struct {
|
||||
Description string
|
||||
InputSchema json.RawMessage
|
||||
Handler func(ctx context.Context, args json.RawMessage) (string, error)
|
||||
Rich func(ctx context.Context, args json.RawMessage) Result
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
Data string `json:"data"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
func (c Content) MarshalJSON() ([]byte, error) {
|
||||
if c.Type == "image" {
|
||||
return json.Marshal(map[string]string{"type": c.Type, "data": c.Data, "mimeType": c.MimeType})
|
||||
}
|
||||
return json.Marshal(map[string]string{"type": c.Type, "text": c.Text})
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Content []Content `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
func TextResult(text string, isError bool) Result {
|
||||
return Result{Content: []Content{{Type: "text", Text: text}}, IsError: isError}
|
||||
}
|
||||
|
||||
func (t Tool) Call(ctx context.Context, args json.RawMessage) Result {
|
||||
if t.Rich != nil {
|
||||
return t.Rich(ctx, args)
|
||||
}
|
||||
text, err := t.Handler(ctx, args)
|
||||
if err != nil {
|
||||
return TextResult(err.Error(), true)
|
||||
}
|
||||
return TextResult(text, false)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@@ -52,16 +87,6 @@ type toolDesc struct {
|
||||
InputSchema json.RawMessage `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type content struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type callResult struct {
|
||||
Content []content `json:"content"`
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) Tool(name string) (Tool, bool) {
|
||||
for _, t := range s.Tools {
|
||||
if t.Name == name {
|
||||
@@ -128,10 +153,5 @@ func (s *Server) call(ctx context.Context, params json.RawMessage) (any, *rpcErr
|
||||
return nil, &rpcError{Code: -32602, Message: fmt.Sprintf("unknown tool: %s", p.Name)}
|
||||
}
|
||||
|
||||
text, err := t.Handler(ctx, p.Arguments)
|
||||
if err != nil {
|
||||
return callResult{Content: []content{{Type: "text", Text: err.Error()}}, IsError: true}, nil
|
||||
}
|
||||
|
||||
return callResult{Content: []content{{Type: "text", Text: text}}}, nil
|
||||
return t.Call(ctx, p.Arguments), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"g.fc.run/misc/byre/internal/clients"
|
||||
)
|
||||
|
||||
func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Header.Get("Byre-Host")
|
||||
if host == "" {
|
||||
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Host"))
|
||||
return
|
||||
}
|
||||
|
||||
ws, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
slog.Warn("accept client websocket", "host", host, "err", err)
|
||||
return
|
||||
}
|
||||
defer ws.CloseNow()
|
||||
|
||||
s.clients.Serve(r.Context(), ws, clients.Client{
|
||||
Login: actor(r),
|
||||
Host: host,
|
||||
Commit: r.Header.Get("Byre-Commit"),
|
||||
ConnectedAt: time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
cmd := exec.CommandContext(r.Context(), "tar", "-czf", "-", "-C", t.Dir, "--", t.Name)
|
||||
cmd.Stdout = w
|
||||
stderr := &bytes.Buffer{}
|
||||
cmd.Stderr = stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
slog.Error("stream client transfer", "transfer", transferID, "dir", t.Dir, "name", t.Name, "err", err, "stderr", stderr.String())
|
||||
panic(http.ErrAbortHandler)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
@@ -41,63 +36,6 @@ func (s *Server) GetInboxItem(w http.ResponseWriter, r *http.Request, itemID str
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) GetRunBundle(w http.ResponseWriter, r *http.Request, itemID string) {
|
||||
item, err := s.store.GetInboxItem(r.Context(), itemID)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
if item.Kind != api.ClientRun {
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("item %s is a %s, not a client_run", itemID, item.Kind))
|
||||
return
|
||||
}
|
||||
if item.State != api.Open {
|
||||
writeError(w, http.StatusConflict, fmt.Errorf("run request %s is %s", itemID, item.State))
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Open(s.store.BundlePath(itemID))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
http.ServeContent(w, r, "", info.ModTime(), f)
|
||||
}
|
||||
|
||||
func (s *Server) ReportRun(w http.ResponseWriter, r *http.Request, itemID string) {
|
||||
req := api.RunReport{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
item, err := s.agents.ReportRun(r.Context(), actor(r), itemID, req)
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (s *Server) removeClosedBundles(events []api.Event) {
|
||||
for _, ev := range events {
|
||||
if ev.Kind != "inbox.updated" || ev.Payload["kind"] != string(api.ClientRun) || ev.Payload["state"] == string(api.Open) {
|
||||
continue
|
||||
}
|
||||
id, _ := ev.Payload["id"].(string)
|
||||
err := os.Remove(s.store.BundlePath(id))
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
slog.Error("remove run bundle", "item", id, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RespondInbox(w http.ResponseWriter, r *http.Request) {
|
||||
req := api.InboxRespond{}
|
||||
if !readJSON(w, r, &req) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"g.fc.run/misc/byre/internal/agents"
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/clients"
|
||||
"g.fc.run/misc/byre/internal/git"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/todo"
|
||||
@@ -26,6 +27,7 @@ type Server struct {
|
||||
store *store.Store
|
||||
agents *agents.Manager
|
||||
hub *hub
|
||||
clients *clients.Registry
|
||||
terminals *terminals
|
||||
startedAt time.Time
|
||||
|
||||
@@ -43,6 +45,7 @@ func New(dataDir string, portFirst int) (*Server, error) {
|
||||
|
||||
s := &Server{
|
||||
hub: newHub(),
|
||||
clients: clients.New(),
|
||||
terminals: newTerminals(),
|
||||
startedAt: time.Now().UTC(),
|
||||
users: map[string]string{},
|
||||
@@ -65,7 +68,7 @@ func New(dataDir string, portFirst int) (*Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
s.agents = agents.New(s.store, filepath.Join(dataDir, "worktrees"), filepath.Join(dataDir, "leads"), filepath.Join(dataDir, "codex"), s.status, s.createProject)
|
||||
s.agents = agents.New(s.store, filepath.Join(dataDir, "worktrees"), filepath.Join(dataDir, "leads"), filepath.Join(dataDir, "codex"), s.status, s.createProject, s.clients)
|
||||
err = s.agents.Recover(context.Background())
|
||||
if err != nil {
|
||||
s.store.Close()
|
||||
@@ -81,7 +84,6 @@ func (s *Server) publish(events []api.Event) {
|
||||
s.hub.publish(ev)
|
||||
}
|
||||
s.agents.Observe(events)
|
||||
s.removeClosedBundles(events)
|
||||
for _, ev := range events {
|
||||
if (ev.Kind == "task.merged" || ev.Kind == "task.discarded") && ev.TaskId != nil {
|
||||
s.terminals.close(*ev.TaskId)
|
||||
@@ -134,7 +136,7 @@ func (s *Server) identify(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
if (r.Method != http.MethodGet && !automaticRequests[r.Pattern]) || humanGets[r.Pattern] {
|
||||
if r.Method != http.MethodGet && !automaticRequests[r.Pattern] {
|
||||
s.acted(login, r)
|
||||
}
|
||||
|
||||
@@ -155,11 +157,6 @@ func (s *Server) clientsChanged() {
|
||||
var automaticRequests = map[string]bool{
|
||||
"PUT /api/me/pane-focus": true,
|
||||
"PUT /api/tasks/{taskId}/summary-read": true,
|
||||
"POST /api/inbox/{itemId}/runs": true,
|
||||
}
|
||||
|
||||
var humanGets = map[string]bool{
|
||||
"GET /api/inbox/{itemId}/bundle": true,
|
||||
}
|
||||
|
||||
func (s *Server) ensureUser(ctx context.Context, login, name string) error {
|
||||
|
||||
+5
-40
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
|
||||
func DeliverableResponse(t api.InboxResponseType) bool {
|
||||
switch t {
|
||||
case api.Option, api.Other, api.Run:
|
||||
case api.Option, api.Other:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -168,31 +167,6 @@ func (s *Store) Respond(ctx context.Context, actor string, resolutions []Resolut
|
||||
|
||||
var ErrNotOpen = errors.New("inbox item is not open")
|
||||
|
||||
func (s *Store) AddRunResponse(ctx context.Context, resp api.InboxResponse) (bool, error) {
|
||||
added := false
|
||||
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO inbox_responses (id, item_id, user_login, type, text, client, exit_code, images, attachments, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, resp.Id, resp.ItemId, resp.User, string(api.Run), resp.Text, *resp.Client, *resp.ExitCode, formatTime(resp.CreatedAt))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil || n == 0 {
|
||||
return err
|
||||
}
|
||||
added = true
|
||||
return emitItem(ctx, tx, emit, "inbox.updated", resp.ItemId, resp.User)
|
||||
})
|
||||
return added, err
|
||||
}
|
||||
|
||||
func (s *Store) BundlePath(itemID string) string {
|
||||
return filepath.Join(s.dataDir(), "bundles", itemID+".tar.gz")
|
||||
}
|
||||
|
||||
func (s *Store) WithdrawItem(ctx context.Context, id string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `UPDATE inbox_items SET state = ?, resolved_at = ? WHERE id = ? AND state = ?`,
|
||||
@@ -261,9 +235,9 @@ type Pending struct {
|
||||
func (s *Store) Undelivered(ctx context.Context, sessionID string) ([]Pending, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT r.id, r.item_id FROM inbox_responses r JOIN inbox_items i ON i.id = r.item_id
|
||||
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?, ?)
|
||||
WHERE i.agent_session_id = ? AND r.delivered_at IS NULL AND r.type IN (?, ?)
|
||||
ORDER BY r.created_at
|
||||
`, sessionID, string(api.Option), string(api.Other), string(api.Run))
|
||||
`, sessionID, string(api.Option), string(api.Other))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -373,7 +347,7 @@ func getItem(ctx context.Context, q querier, id string) (api.InboxItem, error) {
|
||||
|
||||
func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxResponse, error) {
|
||||
rows, err := q.QueryContext(ctx, `
|
||||
SELECT id, item_id, user_login, type, options, text, client, exit_code, images, attachments, created_at, delivered_at
|
||||
SELECT id, item_id, user_login, type, options, text, images, attachments, created_at, delivered_at
|
||||
FROM inbox_responses WHERE item_id = ? ORDER BY created_at
|
||||
`, itemID)
|
||||
if err != nil {
|
||||
@@ -387,24 +361,15 @@ func getResponses(ctx context.Context, q querier, itemID string) ([]api.InboxRes
|
||||
typ := ""
|
||||
options := sql.NullString{}
|
||||
text := sql.NullString{}
|
||||
client := ""
|
||||
exitCode := sql.NullInt64{}
|
||||
images := ""
|
||||
attachments := ""
|
||||
createdAt := ""
|
||||
delivered := sql.NullString{}
|
||||
|
||||
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &client, &exitCode, &images, &attachments, &createdAt, &delivered)
|
||||
err = rows.Scan(&r.Id, &r.ItemId, &r.User, &typ, &options, &text, &images, &attachments, &createdAt, &delivered)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if client != "" {
|
||||
r.Client = &client
|
||||
}
|
||||
if exitCode.Valid {
|
||||
code := int(exitCode.Int64)
|
||||
r.ExitCode = &code
|
||||
}
|
||||
|
||||
r.Type = api.InboxResponseType(typ)
|
||||
if options.Valid {
|
||||
|
||||
@@ -469,6 +469,11 @@ ALTER TABLE agent_sessions ADD COLUMN subagents INTEGER NOT NULL DEFAULT 0;
|
||||
`}, {"add-inbox-run-responses", `
|
||||
ALTER TABLE inbox_responses ADD COLUMN client TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE inbox_responses ADD COLUMN exit_code INTEGER;
|
||||
`}, {"drop-client-runs", `
|
||||
DELETE FROM inbox_responses WHERE item_id IN (SELECT id FROM inbox_items WHERE kind = 'client_run');
|
||||
DELETE FROM inbox_items WHERE kind = 'client_run';
|
||||
ALTER TABLE inbox_responses DROP COLUMN client;
|
||||
ALTER TABLE inbox_responses DROP COLUMN exit_code;
|
||||
`}}
|
||||
|
||||
const (
|
||||
|
||||
Reference in New Issue
Block a user