package skills import ( "bytes" "context" "fmt" "os/exec" "path/filepath" "strings" "time" ) // Executor ejecuta scripts de skills de forma segura con allowlist de interpreters. type Executor struct { allowedInterpreters []string timeout time.Duration } // NewExecutor crea un nuevo Executor con la configuracion dada. // Si allowedInterpreters esta vacio, se usa un default de ["bash", "sh"]. func NewExecutor(allowedInterpreters []string, timeout time.Duration) *Executor { if len(allowedInterpreters) == 0 { allowedInterpreters = []string{"bash", "sh"} } if timeout == 0 { timeout = 60 * time.Second } return &Executor{ allowedInterpreters: allowedInterpreters, timeout: timeout, } } // Run ejecuta un script de skill con los argumentos dados. // scriptPath es la ruta absoluta al script. // args son los argumentos pasados al script. // // El script debe tener una extension reconocida (.sh, .bash, .py, etc.) o // un shebang que indique el interprete. // // Retorna stdout+stderr combinados y error si falla. func (e *Executor) Run(ctx context.Context, scriptPath string, args []string) (string, error) { // Inferir interprete desde extension interpreter, err := e.inferInterpreter(scriptPath) if err != nil { return "", err } // Validar que el interprete esta en la allowlist if !e.isAllowed(interpreter) { return "", fmt.Errorf("interpreter not allowed: %s (allowed: %v)", interpreter, e.allowedInterpreters) } // Construir comando cmdArgs := append([]string{scriptPath}, args...) cmd := exec.CommandContext(ctx, interpreter, cmdArgs...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // Aplicar timeout timeoutCtx, cancel := context.WithTimeout(ctx, e.timeout) defer cancel() cmd = exec.CommandContext(timeoutCtx, interpreter, cmdArgs...) cmd.Stdout = &stdout cmd.Stderr = &stderr err = cmd.Run() output := stdout.String() + stderr.String() if timeoutCtx.Err() == context.DeadlineExceeded { return output, fmt.Errorf("script timeout exceeded (%s)", e.timeout) } if err != nil { return output, fmt.Errorf("script failed: %w", err) } return output, nil } // inferInterpreter detecta el interprete a usar desde la extension del archivo. func (e *Executor) inferInterpreter(path string) (string, error) { ext := strings.ToLower(filepath.Ext(path)) switch ext { case ".sh", ".bash": return "bash", nil case ".py": return "python3", nil case ".rb": return "ruby", nil case ".js": return "node", nil default: return "", fmt.Errorf("unsupported script extension: %s", ext) } } // isAllowed verifica si un interprete esta en la allowlist. func (e *Executor) isAllowed(interpreter string) bool { for _, allowed := range e.allowedInterpreters { if allowed == interpreter { return true } } return false }