package tools import ( "context" "fmt" "os" "regexp" "strings" "sync" "time" "sysagent/internal/policy" ) // transientRule describe un error transitorio que justifica reintentar (§54). // // Solo se incluyen errores que garantizan que el comando NO llegó a hacer // su trabajo (no obtuvo el bloqueo, no resolvió el nombre), para que // reintentar sea seguro incluso con comandos que modifican el sistema. type transientRule struct { re *regexp.Regexp what string backoff []time.Duration // una espera por reintento } var transientRules = []transientRule{ { re: regexp.MustCompile(`(?i)could not get lock /var/lib/(dpkg|apt)|unable to acquire the dpkg frontend lock|unable to lock (the )?(administration|download) directory|waiting for cache lock|is another process using it\?`), what: "package manager lock held by another process", backoff: []time.Duration{5 * time.Second, 15 * time.Second, 30 * time.Second}, }, { re: regexp.MustCompile(`(?i)temporary failure in name resolution|temporary failure resolving|could not resolve host|name or service not known`), what: "temporary DNS failure", backoff: []time.Duration{2 * time.Second, 5 * time.Second}, }, } // retryScale permite acortar las esperas en tests. var retryScale = time.Duration(1) // runWithRetry ejecuta spec y reintenta con backoff ante errores // transitorios conocidos. Nunca reintenta indefinidamente: cada regla tiene // un número máximo de reintentos. Añade también la pista de escalada de // privilegios cuando falla por permisos (§46). func runWithRetry(ctx context.Context, env *Env, spec CommandSpec, display, cwd string, risks []policy.Risk) Result { var notes []string r := RunCommand(ctx, spec) attempts := map[int]int{} for { if r.ExitCode == 0 || r.TimedOut || ctx.Err() != nil { break } rule := -1 out := r.Stderr + "\n" + r.Stdout for i, tr := range transientRules { if tr.re.MatchString(out) { rule = i break } } if rule < 0 || attempts[rule] >= len(transientRules[rule].backoff) { break } base := transientRules[rule].backoff[attempts[rule]] wait := base * retryScale attempts[rule]++ notes = append(notes, fmt.Sprintf("retry %d/%d after %s: %s", attempts[rule], len(transientRules[rule].backoff), base, transientRules[rule].what)) env.Log.Infof("%s: %s; retrying in %s", display, transientRules[rule].what, base) select { case <-ctx.Done(): case <-time.After(wait): } if ctx.Err() != nil { break } r = RunCommand(ctx, spec) } res := commandResult(display, cwd, r) if len(notes) > 0 { res.Output += "\n--- retries ---\n" + strings.Join(notes, "\n") res.Audit = append(res.Audit, "retries", fmt.Sprint(len(notes))) } if r.ExitCode != 0 && permissionDeniedRe.MatchString(r.Stderr+r.Stdout) { res.Output += "\n" + escalationHint() } return res } var permissionDeniedRe = regexp.MustCompile(`(?i)permission denied|operation not permitted|must be (run as )?root|are you root|requires (superuser|root) privileges|superuser privileges required|only root can`) var ( escalationOnce sync.Once escalationText string ) // escalationHint explica al modelo si puede elevar privilegios (§46): no se // asume root porque el proyecto lo pida; se comprueba qué hay disponible y // si funciona sin contraseña (sudo -n / doas -n no preguntan nunca). func escalationHint() string { if os.Geteuid() == 0 { return "hint: the agent already runs as root (euid=0); the denial comes from something else (read-only filesystem, SELinux/AppArmor, immutable attribute, capabilities or a container restriction)." } escalationOnce.Do(func() { var parts []string for _, tool := range []string{"sudo", "doas"} { if _, err := execLookPath(tool); err != nil { continue } r := RunCommand(context.Background(), CommandSpec{Name: tool, Args: []string{"-n", "true"}, Timeout: 5 * time.Second}) parts = append(parts, fmt.Sprintf("%s (passwordless: %t)", tool, r.ExitCode == 0)) } if _, err := execLookPath("su"); err == nil { parts = append(parts, "su (needs the target user's password)") } avail := "none available" if len(parts) > 0 { avail = strings.Join(parts, ", ") } escalationText = fmt.Sprintf("hint: permission denied and the agent is not running as root (uid=%d euid=%d). Privilege escalation: %s. Use it only if the policy and the task justify it; if a password is required, ask the operator with ask_secret (CLI only).", os.Getuid(), os.Geteuid(), avail) }) return escalationText }