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 }