1package main23import (4 "context"5 "errors"6 "flag"7 "fmt"8 "io"9 "os"10 "path/filepath"11 "regexp"12 "strings"13 "time"1415 "github.com/FelineStateMachine/tinyrelay/internal/agentrunner"16)1718func agent(ctx context.Context, args []string, out io.Writer) error {19 fs := flag.NewFlagSet("tiny agent", flag.ContinueOnError)20 fs.SetOutput(out)21 relay := fs.String("relay", "", "Nostr relay WebSocket URL")22 command := fs.String("command", "", "agent executable")23 protocol := fs.String("protocol", "acp", "agent protocol: acp or command (text stdin/stdout)")24 keyEnv := fs.String("key-env", "TINY_AGENT_KEY", "environment variable containing the agent hex key or nsec")25 state := fs.String("state", "", "queue journal path (required)")26 cwd := fs.String("cwd", ".", "agent working directory")27 timeout := fs.Duration("timeout", 30*time.Minute, "maximum task and approval wait")28 var rooms, commandArgs []string29 fs.Func("room", "authorized room id (repeatable)", func(value string) error {30 if !regexp.MustCompile(`^[a-z0-9_-]{1,64}$`).MatchString(value) {31 return errors.New("invalid room id")32 }33 rooms = append(rooms, value)34 return nil35 })36 fs.Func("arg", "argument passed directly to the agent (repeatable)", func(value string) error { commandArgs = append(commandArgs, value); return nil })37 if err := fs.Parse(args); err != nil {38 return err39 }40 if *relay == "" || *command == "" || *state == "" || len(rooms) == 0 {41 return errors.New("agent requires --relay, --room, --command and --state")42 }43 if fs.NArg() != 0 || (*protocol != "acp" && *protocol != "command") || *timeout <= 0 {44 return errors.New("invalid agent options")45 }46 secret, err := secretKey(os.Getenv(*keyEnv))47 if err != nil {48 return fmt.Errorf("agent key: %w", err)49 }50 directory, err := filepath.Abs(*cwd)51 if err != nil {52 return err53 }54 info, err := os.Stat(directory)55 if err != nil || !info.IsDir() {56 return errors.New("agent working directory does not exist")57 }58 // The subprocess does not need the relay signing key. It receives messages59 // through stdio and returns requests and output to this signing boundary.60 environment := []string{}61 for _, entry := range os.Environ() {62 if !strings.HasPrefix(entry, *keyEnv+"=") {63 environment = append(environment, entry)64 }65 }66 log := func(err error) { fmt.Fprintln(out, err) }67 return agentrunner.Serve(ctx, agentrunner.ServiceOptions{RelayURL: *relay, Secret: secret, Rooms: rooms, StatePath: *state, Timeout: *timeout, Log: log, Process: agentrunner.ProcessOptions{Command: *command, Args: commandArgs, Env: environment, CWD: directory, Protocol: *protocol}})68}