package agents import ( "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/mcp" ) const screenshotGuidance = "\n- When a turn changes something you can see, you can show it in your final message: save screenshots with the Playwright tools (pages you serve on this task's ports) or hands' capture with save (apps on this Mac), and embed each as a Markdown image, ![what it shows](), with the path the tool returned. They go to the task's media folder, outside the worktree, so they are never committed, and are deleted when the task closes. Leave out a screenshot that adds nothing to the text." func MediaDir(worktreePath string) string { return worktreePath + "-media" } func (m *Manager) handsDir(sessionID string) string { return filepath.Join(m.handsRoot, sessionID) } // replayd keys ScreenCaptureKit clients by executable path, so two hands // processes run from one path keep cancelling each other's connection. func (m *Manager) handsMCP(sessionID, mediaDir string) (mcp.Stdio, error) { dir := m.handsDir(sessionID) err := os.RemoveAll(dir) if err != nil { return mcp.Stdio{}, err } err = os.MkdirAll(dir, 0o755) if err != nil { return mcp.Stdio{}, err } bin, err := copyHands(dir) if err != nil { return mcp.Stdio{}, err } return mcp.Stdio{Name: "hands", Command: bin, Args: []string{"--media-dir", mediaDir}}, nil } func copyHands(dir string) (string, error) { src, err := exec.LookPath("byre-hands") if err != nil { return "", err } data, err := os.ReadFile(src) if err != nil { return "", err } bin := filepath.Join(dir, "byre-hands") return bin, os.WriteFile(bin, data, 0o755) } func (m *Manager) CheckPermissions(ctx context.Context) ([]api.Permission, error) { err := os.MkdirAll(m.handsRoot, 0o755) if err != nil { return nil, err } dir, err := os.MkdirTemp(m.handsRoot, "check-") if err != nil { return nil, err } defer os.RemoveAll(dir) bin, err := copyHands(dir) if err != nil { return nil, err } ctx, cancel := context.WithTimeout(ctx, 50*time.Second) defer cancel() cmd := exec.CommandContext(ctx, bin, "check") stderr := strings.Builder{} cmd.Stderr = &stderr out, err := cmd.Output() if ctx.Err() == context.DeadlineExceeded { return nil, fmt.Errorf("byre-hands check didn't finish within 50s; answer macOS's prompt on the server's Mac and check again") } if err != nil { return nil, fmt.Errorf("byre-hands check: %w: %s", err, strings.TrimSpace(stderr.String())) } perms := []api.Permission{} err = json.Unmarshal(out, &perms) if err != nil { return nil, fmt.Errorf("parse byre-hands check: %w", err) } return perms, nil }