package actions import ( "context" "fmt" "log" "github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/pkg/config" "github.com/sashabaranov/go-openai/jsonschema" "golang.org/x/crypto/ssh" ) func NewShell(config map[string]string) *ShellAction { return &ShellAction{ privateKey: config["privateKey"], user: config["user"], host: config["host"], customName: config["customName"], customDescription: config["customDescription"], } } type ShellAction struct { privateKey string user, host string customName string customDescription string } func (a *ShellAction) Run(ctx context.Context, params types.ActionParams) (types.ActionResult, error) { result := struct { Command string `json:"command"` Host string `json:"host"` User string `json:"user"` }{} err := params.Unmarshal(&result) if err != nil { fmt.Printf("error: %v", err) return types.ActionResult{}, err } if a.host != "" && a.user != "" { result.Host = a.host result.User = a.user } output, err := sshCommand(a.privateKey, result.Command, result.User, result.Host) if err != nil { return types.ActionResult{}, err } return types.ActionResult{Result: output}, nil } func (a *ShellAction) Definition() types.ActionDefinition { name := "shell" description := "Run a shell command on a remote server." if a.customName != "" { name = a.customName } if a.customDescription != "" { description = a.customDescription } if a.host != "" && a.user != "" { return types.ActionDefinition{ Name: types.ActionDefinitionName(name), Description: description, Properties: map[string]jsonschema.Definition{ "command": { Type: jsonschema.String, Description: "The command to run on the remote server.", }, }, Required: []string{"command"}, } } return types.ActionDefinition{ Name: types.ActionDefinitionName(name), Description: description, Properties: map[string]jsonschema.Definition{ "command": { Type: jsonschema.String, Description: "The command to run on the remote server.", }, "host": { Type: jsonschema.String, Description: "The host of the remote server. e.g. ip:port", }, "user": { Type: jsonschema.String, Description: "The user to connect to the remote server.", }, }, Required: []string{"command", "host", "user"}, } } // ShellConfigMeta returns the metadata for Shell action configuration fields func ShellConfigMeta() []config.Field { return []config.Field{ { Name: "privateKey", Label: "Private Key", Type: config.FieldTypeTextarea, Required: true, HelpText: "SSH private key for connecting to remote servers", }, { Name: "user", Label: "Default User", Type: config.FieldTypeText, HelpText: "Default SSH user for connecting to remote servers", }, { Name: "host", Label: "Default Host", Type: config.FieldTypeText, HelpText: "Default host for SSH connections (e.g., hostname:port)", }, { Name: "customName", Label: "Custom Action Name", Type: config.FieldTypeText, HelpText: "Custom name for this action", }, { Name: "customDescription", Label: "Custom Description", Type: config.FieldTypeTextarea, HelpText: "Custom description for this action", }, } } func sshCommand(privateKey, command, user, host string) (string, error) { // Create signer from private key string key, err := ssh.ParsePrivateKey([]byte(privateKey)) if err != nil { log.Fatalf("failed to parse private key: %v", err) } // SSH client configuration config := &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ ssh.PublicKeys(key), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } // Connect to SSH server client, err := ssh.Dial("tcp", host, config) if err != nil { return "", fmt.Errorf("failed to dial: %v", err) } defer client.Close() // Open a new session session, err := client.NewSession() if err != nil { return "", fmt.Errorf("failed to create session: %v", err) } defer session.Close() // Run a command output, err := session.CombinedOutput(command) if err != nil { return "", fmt.Errorf("failed to run: %v", err) } return string(output), nil } func (a *ShellAction) Plannable() bool { return true }