post inicial
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user