package security import ( "fmt" "os" "os/exec" "strings" "sync" "time" ) // ShellNoOutputTimeoutMessage 长时间无新 stdout/stderr 时的提示(软失败,模型可见)。 func ShellNoOutputTimeoutMessage(idleSec int) string { return fmt.Sprintf(`命令已终止:超过 %d 秒没有新的输出,疑似在等待交互输入或已挂起。 长时静默任务请使用末尾 & 后台运行,或增大 agent.shell_no_output_timeout_seconds(-1=关闭此检测)。 Command terminated: no new output for %d seconds (possible interactive wait or hung process).`, idleSec, idleSec) } // ShellInactivityWatch 在 noOutputSec 内无任何新输出时向 expired 发送信号;每次 Bump 重置计时。 // 与「仅有首包输出就永久取消计时」不同,可兜住 sudo 打印 Password 提示后继续挂起等情况。 type ShellInactivityWatch struct { Sec int mu sync.Mutex timer *time.Timer Expired chan struct{} } func NewShellInactivityWatch(noOutputSec int) *ShellInactivityWatch { sec := ResolveShellNoOutputTimeoutSeconds(noOutputSec) if sec <= 0 { return nil } w := &ShellInactivityWatch{ Sec: sec, Expired: make(chan struct{}, 1), } w.Bump() return w } func (w *ShellInactivityWatch) Bump() { if w == nil || w.Sec <= 0 { return } w.mu.Lock() defer w.mu.Unlock() if w.timer != nil { w.timer.Stop() } w.timer = time.AfterFunc(time.Duration(w.Sec)*time.Second, func() { select { case w.Expired <- struct{}{}: default: } }) } func (w *ShellInactivityWatch) Stop() { if w == nil { return } w.mu.Lock() defer w.mu.Unlock() if w.timer != nil { w.timer.Stop() w.timer = nil } } // ResolveShellNoOutputTimeoutSeconds:0=默认 300(5 分钟);-1=关闭;>0=自定义。 func ResolveShellNoOutputTimeoutSeconds(sec int) int { if sec < 0 { return 0 } if sec == 0 { return 300 } return sec } // PrependNonInteractiveShellExports 为 sh -c 注入通用非交互环境(pager 等),不维护命令黑名单。 func PrependNonInteractiveShellExports(shellCommand string) string { if strings.TrimSpace(shellCommand) == "" { return shellCommand } upper := strings.ToUpper(shellCommand) var pairs []string add := func(key, val string) { if strings.Contains(upper, strings.ToUpper(key)) { return } pairs = append(pairs, key+"="+val) } add("GIT_PAGER", "cat") add("PAGER", "cat") add("SYSTEMD_PAGER", "cat") add("DEBIAN_FRONTEND", "noninteractive") if len(pairs) == 0 { return shellCommand } return "export " + strings.Join(pairs, " ") + "\n" + shellCommand } // PrependNonInteractiveStdinRedirect 为 sh -c 关闭 stdin(与 attachNonInteractiveStdin 等价), // 使 read/input()/sudo -S 等从 stdin 读取的程序快速失败而非挂起。已含