3638529468
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/.
111 lines
2.8 KiB
Go
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
|
|
}
|