105 lines
4.0 KiB
Go
105 lines
4.0 KiB
Go
package project
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// MaxInstructionSize limita agent.md y cada archivo de tarea. Un archivo
|
|
// de instrucciones de varios MiB es casi seguro un error (o un intento de
|
|
// agotar el contexto) y no debe cargarse en el prompt.
|
|
const MaxInstructionSize = 1 << 20
|
|
|
|
// Finding es un resultado de la revisión de seguridad del proyecto.
|
|
type Finding struct {
|
|
Fatal bool // impide arrancar
|
|
Message string
|
|
}
|
|
|
|
// CheckSecurity revisa permisos y propietarios de lo que da instrucciones o
|
|
// credenciales al agente.
|
|
//
|
|
// Motivo: si otro usuario puede escribir agent.md, init.conf, un archivo de
|
|
// tarea o el propio PROJECT_ROOT (para crear tareas nuevas), puede decidir
|
|
// qué hace el agente. Si el agente corre como root, eso es una escalada de
|
|
// privilegios. Por eso:
|
|
//
|
|
// - escribible por "otros" (o+w): error fatal;
|
|
// - escribible por el grupo (g+w): advertencia;
|
|
// - con el agente como root, propietario distinto de root: advertencia;
|
|
// - init.conf legible por grupo/otros con API_KEY definida: advertencia.
|
|
func (p *Project) CheckSecurity(agentFile string, tasks []Task, hasAPIKey bool) []Finding {
|
|
var out []Finding
|
|
euid := os.Geteuid()
|
|
check := func(path, what string) {
|
|
st, err := os.Stat(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
mode := st.Mode()
|
|
sticky := mode&os.ModeSticky != 0
|
|
switch {
|
|
case mode.Perm()&0o002 != 0 && !(mode.IsDir() && sticky):
|
|
out = append(out, Finding{Fatal: true, Message: fmt.Sprintf("%s %s is writable by any user (mode %04o): anyone could control the agent; fix with chmod o-w", what, path, mode.Perm())})
|
|
case mode.IsDir() && sticky && mode.Perm()&0o002 != 0:
|
|
out = append(out, Finding{Fatal: true, Message: fmt.Sprintf("%s %s is a shared world-writable directory (like /tmp): any user could add task files; use a private directory", what, path)})
|
|
case mode.Perm()&0o020 != 0:
|
|
out = append(out, Finding{Message: fmt.Sprintf("%s %s is group-writable (mode %04o); members of its group can change the agent's instructions", what, path, mode.Perm())})
|
|
}
|
|
if sys, ok := st.Sys().(*syscall.Stat_t); ok && euid == 0 && sys.Uid != 0 {
|
|
out = append(out, Finding{Message: fmt.Sprintf("%s %s is owned by uid %d but the agent runs as root: that user controls a root process", what, path, sys.Uid)})
|
|
}
|
|
}
|
|
check(p.Root, "PROJECT_ROOT")
|
|
check(p.Path(agentFile), "agent file")
|
|
check(p.Path("init.conf"), "init.conf")
|
|
for _, t := range tasks {
|
|
check(t.Path, "task file")
|
|
}
|
|
if hasAPIKey {
|
|
if st, err := os.Stat(p.Path("init.conf")); err == nil && st.Mode().Perm()&0o044 != 0 {
|
|
out = append(out, Finding{Message: fmt.Sprintf("init.conf contains API_KEY and is readable by other users (mode %04o); fix with chmod 600 init.conf", st.Mode().Perm())})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ErrLocked indica que otra instancia usa el mismo PROJECT_ROOT.
|
|
var ErrLocked = errors.New("another sysagent instance is running in this PROJECT_ROOT")
|
|
|
|
// Lock impide que dos instancias trabajen a la vez sobre el mismo proyecto,
|
|
// lo que corrompería contexto.mem y podría duplicar acciones sobre el
|
|
// sistema. Usa flock(2): el bloqueo desaparece solo si el proceso muere.
|
|
// Devuelve la función que lo libera.
|
|
func (p *Project) Lock() (unlock func(), err error) {
|
|
path := filepath.Join(p.Root, WorkDir, "lock")
|
|
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open lock file %s: %w", path, err)
|
|
}
|
|
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
|
|
data := make([]byte, 32)
|
|
n, _ := f.ReadAt(data, 0)
|
|
f.Close()
|
|
if errors.Is(err, syscall.EWOULDBLOCK) {
|
|
if pid := strings.TrimSpace(string(data[:n])); pid != "" {
|
|
return nil, fmt.Errorf("%w (pid %s)", ErrLocked, pid)
|
|
}
|
|
return nil, ErrLocked
|
|
}
|
|
return nil, fmt.Errorf("failed to lock %s: %w", path, err)
|
|
}
|
|
f.Truncate(0)
|
|
f.WriteAt([]byte(strconv.Itoa(os.Getpid())+"\n"), 0)
|
|
return func() {
|
|
f.Truncate(0)
|
|
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
|
|
f.Close()
|
|
}, nil
|
|
}
|