mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
三层回退恢复机制(L0写前留档/L1恢复梯子/L2离线回滚)+ guard 父守护
- 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 单元测试全过
This commit is contained in:
@ -12,6 +12,7 @@ import (
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/system"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -26,6 +27,7 @@ type Plugin struct {
|
||||
sdk *sdk.PluginSDK
|
||||
mu sync.RWMutex
|
||||
filesDir string
|
||||
baseDir string // L0 写前留档根目录(<data>/file_baseline 的父目录),空则禁用
|
||||
}
|
||||
|
||||
func New(name string) *Plugin {
|
||||
@ -57,6 +59,29 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
}
|
||||
p.filesDir = abs
|
||||
|
||||
// L0 写前留档:主 agent 写 /etc 等受保护路径前自动存档原文(homed 注入 <data>)
|
||||
if cfg := s.Config().Get(); cfg != nil {
|
||||
if dd := cfg.Daemon.DataDir; dd != "" {
|
||||
p.baseDir = dd
|
||||
}
|
||||
}
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "baseline_dir",
|
||||
Default: p.baseDir,
|
||||
Type: "string",
|
||||
DisplayName: "写前留档目录",
|
||||
Description: "写受保护系统路径前自动存档原文的目录(空禁用)",
|
||||
Category: "files",
|
||||
})
|
||||
if v, err := s.Settings().Get("baseline_dir"); err == nil && v != nil {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
p.baseDir = s
|
||||
}
|
||||
}
|
||||
if p.baseDir != "" {
|
||||
log.Printf("[%s] write-ahead baseline dir: %s", p.name, system.FileBaselineDir(p.baseDir))
|
||||
}
|
||||
|
||||
tp := p.name + "_"
|
||||
|
||||
s.RegisterTool(tp+"read", sdk.ToolDef{
|
||||
@ -251,6 +276,15 @@ func (p *Plugin) handleWrite(args map[string]interface{}) (interface{}, error) {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
|
||||
// L0 写前留档:覆盖已有受保护文件前,原文存档供 guard 还原。
|
||||
if mode != "create" {
|
||||
if archived, aerr := p.archiveBeforeWrite(absPath); aerr != nil {
|
||||
log.Printf("[%s] write-ahead archive %s: %v", p.name, absPath, aerr)
|
||||
} else if archived {
|
||||
log.Printf("[%s] write-ahead archived %s", p.name, absPath)
|
||||
}
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "create":
|
||||
if _, err := os.Stat(absPath); err == nil {
|
||||
@ -337,6 +371,13 @@ func (p *Plugin) handleEdit(args map[string]interface{}) (interface{}, error) {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
|
||||
// L0 写前留档(edit 可能覆盖已存在文件)
|
||||
if archived, aerr := p.archiveBeforeWrite(absPath); aerr != nil {
|
||||
log.Printf("[%s] write-ahead archive %s: %v", p.name, absPath, aerr)
|
||||
} else if archived {
|
||||
log.Printf("[%s] write-ahead archived %s", p.name, absPath)
|
||||
}
|
||||
|
||||
rawEdits, ok := args["edits"].([]interface{})
|
||||
if !ok || len(rawEdits) == 0 {
|
||||
return errorResult("edits must be a non-empty array"), nil
|
||||
@ -481,6 +522,14 @@ func errorResult(msg string) map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
// archiveBeforeWrite L0 写前留档:若目标为受保护系统路径且已存在,则存档原文。
|
||||
func (p *Plugin) archiveBeforeWrite(absPath string) (bool, error) {
|
||||
if p.baseDir == "" {
|
||||
return false, nil
|
||||
}
|
||||
return system.ArchiveBeforeWrite(p.baseDir, absPath)
|
||||
}
|
||||
|
||||
func getSetting[T any](s sdk.SettingsAPI, key string, def T) T {
|
||||
v, err := s.Get(key)
|
||||
if err != nil || v == nil {
|
||||
|
||||
Reference in New Issue
Block a user