mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-03 23:54:12 +00:00
fix(examples): 全插件安全审查修复(qq/a2a/memo/calendar/rss/browser/bili/recoverydiag)
审查发现并修复 7 项问题: - P1 qq: downloadURL 裸 http.Get 无超时 → 120s client - P2 a2a: inbound http.Server 零超时 → Read 30s/Write 120s/Idle 60s - P3 bili: output_dir 配置项零校验 → 系统目录黑名单(/、/etc、/usr、/var 等) - P4 recoverydiag: db_path LLM 可控任意 sqlite → 强制限制 data 目录内 - P5 memo/calendar/rss: os.WriteFile 直写 → atomicWriteJSON (temp+rename) - P6 qq: 3 处后台 goroutine(已读/rcon转发/下载)加 panic recover - P7 browser: dump-dom failback Kill 后补 wait 回收僵尸进程 recoverydiag 此前被 .gitignore 排除,但其 db_path 安全修复 属生产代码,故取消忽略并入库。 全部经 plugindev 重打包升版安装验证 config_kept=true。
This commit is contained in:
153
example/recoverydiag/diag_test.go
Normal file
153
example/recoverydiag/diag_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var realCfg = "/home/newqqagent/config.db"
|
||||
var realLog = "/home/newqqagent/log"
|
||||
|
||||
func TestDiagTriage(t *testing.T) {
|
||||
p := &Plugin{name: "recoverydiag"}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args map[string]interface{}
|
||||
want string
|
||||
}{
|
||||
{"signal", map[string]interface{}{"exit_code": 0, "signal": "SIGSEGV"}, "process_death"},
|
||||
{"oom", map[string]interface{}{"exit_code": 0, "signal": "SIGKILL", "crash_reason": "oom-kill"}, "process_starvation"},
|
||||
{"nonzero", map[string]interface{}{"exit_code": 1}, "process_death"},
|
||||
{"healthy", map[string]interface{}{"exit_code": 0}, "normal_stop"},
|
||||
{"alive", map[string]interface{}{"still_alive": true, "signal": "SIGKILL"}, "config_unreachable"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
r, _ := p.handleTriage(c.args)
|
||||
m, ok := r.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("%s: not a map", c.name)
|
||||
}
|
||||
if got, _ := m["class"].(string); got != c.want {
|
||||
t.Errorf("%s: class = %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagDB(t *testing.T) {
|
||||
if _, err := os.Stat(realCfg); err != nil {
|
||||
t.Skip("config.db not present, skipping")
|
||||
}
|
||||
p := &Plugin{name: "recoverydiag"}
|
||||
r, err := p.handleDB(map[string]interface{}{"db_path": realCfg})
|
||||
if err != nil {
|
||||
t.Fatalf("handleDB: %v", err)
|
||||
}
|
||||
m := r.(map[string]interface{})
|
||||
t.Logf("integrity=%v sources=%v verdict=%v summary=%v", m["integrity"], m["source_count"], m["verdict"], m["summary"])
|
||||
if m["integrity"] != "ok" {
|
||||
t.Errorf("integrity = %v, want ok", m["integrity"])
|
||||
}
|
||||
if m["source_count"] == 0 {
|
||||
t.Errorf("source_count == 0, expected LLM sources")
|
||||
}
|
||||
if got, _ := m["source_failed"].(int); got != 0 {
|
||||
t.Errorf("source_failed = %d, want 0 (all sources OK): %v", got, m["missing_fields"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagLogScan(t *testing.T) {
|
||||
if _, err := os.Stat(realLog); err != nil {
|
||||
t.Skip("log dir not present, skipping")
|
||||
}
|
||||
p := &Plugin{name: "recoverydiag"}
|
||||
r, err := p.handleLogScan(map[string]interface{}{
|
||||
"log_dir": realLog,
|
||||
"since_minutes": 60 * 24 * 3,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("handleLogScan: %v", err)
|
||||
}
|
||||
m := r.(map[string]interface{})
|
||||
t.Logf("matched=%v counts=%v dominant=%v conclusion=%v", m["lines_matched"], m["counts"], m["dominant"], m["conclusion"])
|
||||
}
|
||||
|
||||
func TestDiagDelta(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
cur := t.TempDir()
|
||||
sub := filepath.Join(base, "sub")
|
||||
os.MkdirAll(sub, 0755)
|
||||
|
||||
// modified: same path, different content
|
||||
os.WriteFile(filepath.Join(base, "a.txt"), []byte("hello"), 0644)
|
||||
os.WriteFile(filepath.Join(cur, "a.txt"), []byte("world!"), 0644)
|
||||
// created
|
||||
os.WriteFile(filepath.Join(cur, "b.txt"), []byte("new"), 0644)
|
||||
// deleted
|
||||
os.WriteFile(filepath.Join(base, "gone.txt"), []byte("bye"), 0644)
|
||||
// unchanged
|
||||
os.WriteFile(filepath.Join(base, "same.txt"), []byte("x"), 0644)
|
||||
os.WriteFile(filepath.Join(cur, "same.txt"), []byte("x"), 0644)
|
||||
|
||||
p := &Plugin{name: "recoverydiag"}
|
||||
r, err := p.handleDelta(map[string]interface{}{"baseline_dir": base, "current_dir": cur})
|
||||
if err != nil {
|
||||
t.Fatalf("handleDelta: %v", err)
|
||||
}
|
||||
m := r.(map[string]interface{})
|
||||
sum := m["summary"].(map[string]int)
|
||||
t.Logf("summary=%v total=%v", sum, m["total_diff"])
|
||||
if sum["created"] != 1 || sum["deleted"] != 1 || sum["modified"] != 1 {
|
||||
t.Errorf("summary = %v, want modified=1 created=1 deleted=1", sum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagLoc(t *testing.T) {
|
||||
p := &Plugin{name: "recoverydiag"}
|
||||
r, _ := p.handleLoc(map[string]interface{}{
|
||||
"triage": map[string]interface{}{"class": "process_death", "verdict": "down"},
|
||||
"db": map[string]interface{}{"verdict": "ok"},
|
||||
"log_scan": map[string]interface{}{"dominant": "panic"},
|
||||
"delta": map[string]interface{}{"summary": map[string]interface{}{"created": 0, "modified": 0, "deleted": 0}},
|
||||
})
|
||||
m := r.(map[string]interface{})
|
||||
// 经 JSON 往返,模拟内核把子结论以 JSON 传给 diag_loc 的真实路径
|
||||
raw, _ := json.Marshal(m)
|
||||
var dec map[string]interface{}
|
||||
json.Unmarshal(raw, &dec)
|
||||
hs := dec["ranked_hypotheses"].([]interface{})
|
||||
if len(hs) == 0 {
|
||||
t.Fatal("no hypotheses")
|
||||
}
|
||||
top := hs[0].(map[string]interface{})
|
||||
t.Logf("top cause=%v conf=%v rec=%v", top["cause"], top["confidence"], top["recommendation"])
|
||||
if top["cause"] != "code_panic_loop" {
|
||||
t.Errorf("expected code_panic_loop, got %v", top["cause"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagLocPersist(t *testing.T) {
|
||||
kb := filepath.Join(t.TempDir(), "recovery_kb")
|
||||
p := &Plugin{name: "recoverydiag", dataDir: filepath.Dir(kb)}
|
||||
args := map[string]interface{}{
|
||||
"persist": true,
|
||||
"triage": map[string]interface{}{"class": "process_death", "verdict": "down"},
|
||||
"db": map[string]interface{}{"verdict": "ok"},
|
||||
"log_scan": map[string]interface{}{"dominant": "panic"},
|
||||
"delta": map[string]interface{}{"summary": map[string]interface{}{"created": 0, "modified": 0, "deleted": 0}},
|
||||
}
|
||||
if _, err := p.handleLoc(args); err != nil {
|
||||
t.Fatalf("handleLoc: %v", err)
|
||||
}
|
||||
entries, err := os.ReadDir(kb)
|
||||
if err != nil || len(entries) == 0 {
|
||||
t.Fatalf("expected persisted diag json, got err=%v entries=%v", err, entries)
|
||||
}
|
||||
data, _ := os.ReadFile(filepath.Join(kb, entries[0].Name()))
|
||||
if !strings.Contains(string(data), `"cause"`) {
|
||||
t.Errorf("persisted file missing cause field: %s", data)
|
||||
}
|
||||
}
|
||||
21
example/recoverydiag/plg.json
Normal file
21
example/recoverydiag/plg.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "recoverydiag",
|
||||
"name_zh": "恢复诊断",
|
||||
"name_en": "Recovery Diagnostics",
|
||||
"version": "0.2.0",
|
||||
"description": "快速检查/崩溃取证工具集:diag_triage(退出码/信号/存活粗分)、diag_db(config.db 完整性 + LLM 源解析校验)、diag_log_scan(日志签名命中)、diag_delta(last-good 快照 vs 现状 diff)、diag_loc(正交综合定位)。全部返回结论而非原文,确定性、不消耗 LLM token,供 guard / failback 恢复决策使用。",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": [
|
||||
"diag",
|
||||
"recovery",
|
||||
"diagnostics",
|
||||
"triage",
|
||||
"failback"
|
||||
],
|
||||
"targets": "linux/amd64",
|
||||
"outdir": "dist",
|
||||
"bundle": true,
|
||||
"replaces": {},
|
||||
"source_dirs": []
|
||||
}
|
||||
1023
example/recoverydiag/plugin.go
Normal file
1023
example/recoverydiag/plugin.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user