mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
- L0: files 插件写受保护系统路径(/etc 等)前自动留档,AbstractBeforeWrite 到 data/file_baseline - L1: failback 受限 worker 执行恢复梯子 probe→还原DNS/proxy→还原LLM配置+ReloadFromConfig→probe,N轮有界 - L2: tracker changeset 持久化原文 blob,guard 离线 RollbackFromDisk 回滚 agentfs;SystemSnapshot 支撑 - guard 父守护: 心跳 IPC(PING/ACK unix socket, 文件心跳回退)、失败计数、退出码协议(42/43/44)、最后手段 - 发行版路径适配: system.protected_paths/network_paths 可注入,默认面向主流 Linux - Windows 兼容: guard.go/failback.go 加 //go:build linux, guard_windows.go 提供 no-op 桩 - 修复: guard.yaml last_resort 键冲突、changeset Content 不落盘导致离线回滚丢原文 Build 全绿, vet 干净, system/recovery/ipc/tracker 单元测试全过
146 lines
4.0 KiB
Go
146 lines
4.0 KiB
Go
package system
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
// ProtectedPaths L0 写前留档保护的系统路径前缀。主 agent 通过 files 插件写
|
||
// 这些路径前,先自动把原文存档到 <dataDir>/file_baseline/,供 guard 还原。
|
||
//
|
||
// 该集合强依赖当前 Linux 发行版布局(/etc、/home)。默认值仅作兜底;
|
||
// 生产环境应由 homed/main 或 guard.yaml 显式注入(SetProtectedPaths),
|
||
// 以适配具体发行版/部署路径,避免硬编码失效。
|
||
var protectedPrefixes = []string{
|
||
"/etc/",
|
||
}
|
||
|
||
// SetProtectedPaths 覆盖受保护路径前缀集合(发行版/部署路径适配入口)。
|
||
// 幂等地保留 / 前缀兜底;传入 nil/空则恢复默认。
|
||
func SetProtectedPaths(prefixes []string) {
|
||
if len(prefixes) == 0 {
|
||
protectedPrefixes = []string{"/etc/"}
|
||
return
|
||
}
|
||
ns := make([]string, 0, len(prefixes))
|
||
for _, p := range prefixes {
|
||
if p = strings.TrimSpace(p); p != "" {
|
||
ns = append(ns, p)
|
||
}
|
||
}
|
||
protectedPrefixes = ns
|
||
}
|
||
|
||
// ProtectedPaths 返回当前受保护路径前缀(副本)。
|
||
func ProtectedPaths() []string {
|
||
out := make([]string, len(protectedPrefixes))
|
||
copy(out, protectedPrefixes)
|
||
return out
|
||
}
|
||
|
||
// IsProtectedPath 判断路径是否属于受保护的写前留档范围。
|
||
func IsProtectedPath(path string) bool {
|
||
abs, err := filepath.Abs(path)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
for _, p := range protectedPrefixes {
|
||
if strings.HasPrefix(abs, p) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// IsProtectedPathExplicit 同 IsProtectedPath,但仅匹配传入的显式前缀集
|
||
// (不改全局状态时的判断入口,供测试/工具使用)。
|
||
func IsProtectedPathExplicit(prefixes []string, path string) bool {
|
||
abs, err := filepath.Abs(path)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
for _, p := range prefixes {
|
||
if strings.HasPrefix(abs, p) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ArchiveBeforeWrite 在覆盖某个受保护路径前调用:若目标为已存在的普通文件,
|
||
// 则把原文复制到 <dataDir>/file_baseline/<相对路径>(幂等:已存在同 hash 则跳过),
|
||
// 返回是否发生了留档。用于 L0 写 /etc 前的自动留档。
|
||
//
|
||
// archive base 为 <dataDir>/file_baseline。path 为绝对路径。
|
||
func ArchiveBeforeWrite(baseDir, path string) (archived bool, err error) {
|
||
if !IsProtectedPath(path) {
|
||
return false, nil
|
||
}
|
||
info, err := os.Stat(path)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return false, nil // 新建文件无需留档
|
||
}
|
||
return false, err
|
||
}
|
||
if info.IsDir() {
|
||
return false, nil
|
||
}
|
||
|
||
rel := strings.TrimPrefix(path, "/")
|
||
dst := filepath.Join(filepath.Join(baseDir, "file_baseline"), rel)
|
||
if _, err := os.Stat(dst); err == nil {
|
||
// 已留档(内容未再变化时避免重复)
|
||
if same, _ := sameContent(path, dst); same {
|
||
return false, nil
|
||
}
|
||
}
|
||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||
return false, err
|
||
}
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if err := os.WriteFile(dst, data, 0600); err != nil {
|
||
return false, err
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
func sameContent(a, b string) (bool, error) {
|
||
da, ea := os.ReadFile(a)
|
||
if ea != nil {
|
||
return false, ea
|
||
}
|
||
db, eb := os.ReadFile(b)
|
||
if eb != nil {
|
||
return false, eb
|
||
}
|
||
return string(da) == string(db), nil
|
||
}
|
||
|
||
// FileBaselineDir 返回写前留档根目录。
|
||
func FileBaselineDir(dataDir string) string {
|
||
return filepath.Join(dataDir, "file_baseline")
|
||
}
|
||
|
||
// RestoreFileFromBaseline 从写前留档恢复一个受保护文件(guard 使用)。
|
||
// 若留档存在则写回并返回 true 与恢复路径;否则 false。
|
||
func RestoreFileFromBaseline(dataDir, path string) (bool, error) {
|
||
if !IsProtectedPath(path) {
|
||
return false, nil
|
||
}
|
||
rel := strings.TrimPrefix(path, "/")
|
||
dst := filepath.Join(FileBaselineDir(dataDir), rel)
|
||
data, err := os.ReadFile(dst)
|
||
if err != nil {
|
||
return false, nil
|
||
}
|
||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||
return false, fmt.Errorf("restore from baseline %s: %w", dst, err)
|
||
}
|
||
return true, nil
|
||
} |