Files
agents_and_robots/shell/skills/executor.go
T
egutierrez 3638529468 feat: loader y executor de skills en shell
Agregar componentes impuros para manejo de skills en shell/skills/:

Loader (filesystem I/O):
- LoadMeta(): carga metadata de todas las skills
- LoadSkill(): carga skill completa con instrucciones
- ReadResource(): lee recursos con path traversal protection
- Parsing de SKILL.md con frontmatter YAML

Executor (script execution):
- Ejecucion segura de scripts con allowlist de interpreters
- Timeout obligatorio por script
- Inferencia de interpreter desde extension
- Proteccion contra scripts maliciosos

Incluye tests completos con tmpdir para loader y executor.

Arquitectura: impure shell, todo I/O aislado en shell/.
2026-03-08 22:13:12 +00:00

111 lines
2.8 KiB
Go

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
}