2026-10-17 22:43:38 -07:00
package agents
2026-10-18 09:20:01 -07:00
import (
2026-10-18 22:17:54 -07:00
"context"
"encoding/json"
"fmt"
2026-10-18 09:20:01 -07:00
"os"
"os/exec"
"path/filepath"
2026-10-18 22:17:54 -07:00
"strings"
"time"
2026-10-18 09:20:01 -07:00
2026-10-18 22:17:54 -07:00
"g.fc.run/ai/byre/internal/api"
2026-10-18 15:32:07 -07:00
"g.fc.run/ai/byre/internal/mcp"
2026-10-18 09:20:01 -07:00
)
2026-10-17 22:43:38 -07:00
2026-10-18 00:39:04 -07:00
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, , 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"
}
2026-10-18 09:20:01 -07:00
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 ) {
2026-10-18 22:17:54 -07:00
dir := m . handsDir ( sessionID )
err := os . RemoveAll ( dir )
2026-10-18 09:20:01 -07:00
if err != nil {
return mcp . Stdio {}, err
}
2026-10-18 22:17:54 -07:00
err = os . MkdirAll ( dir , 0 o755 )
2026-10-18 09:20:01 -07:00
if err != nil {
return mcp . Stdio {}, err
}
2026-10-18 22:17:54 -07:00
bin , err := copyHands ( dir )
2026-10-18 09:20:01 -07:00
if err != nil {
return mcp . Stdio {}, err
}
2026-10-18 22:17:54 -07:00
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" )
2026-10-18 09:20:01 -07:00
if err != nil {
2026-10-18 22:17:54 -07:00
return "" , err
}
data , err := os . ReadFile ( src )
if err != nil {
return "" , err
2026-10-18 09:20:01 -07:00
}
bin := filepath . Join ( dir , "byre-hands" )
2026-10-18 22:17:54 -07:00
return bin , os . WriteFile ( bin , data , 0 o755 )
}
func ( m * Manager ) CheckPermissions ( ctx context . Context ) ([] api . Permission , error ) {
err := os . MkdirAll ( m . handsRoot , 0 o755 )
2026-10-18 09:20:01 -07:00
if err != nil {
2026-10-18 22:17:54 -07:00
return nil , err
2026-10-18 09:20:01 -07:00
}
2026-10-18 22:17:54 -07:00
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
2026-10-17 22:43:38 -07:00
}