mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-01 14:53:12 +00:00
fix(tools): 审计器补齐 recv/selfCalls 索引与只读白名单;标注器定位修正
## audit_parallel 的四个误判来源
**① indexMethods 没设 recv 和 selfCalls ⇒ 递归跟进空转**
`scanWritesDeep` 靠 recv 组候选键、靠 selfCalls 才知道跟进谁,缺任一个
就静默漏判。handleRestart 只调 p.stopServer/startServer 两个方法,
因此被判成"只读" —— 而 stopServer 里有 `p.server = nil`。
(我先补了闭包那条分支的 selfCalls,误以为问题在闭包上,其实是
indexMethods 整体缺字段。)
**② writePatterns 写了 ".Set(" 而 callString 收集的是方法名**
`p.sdk.Settings().Set(...)` 是三段链,callString 返回 "Settings.Set",
永远匹配不上 ".Set("。漏判比误判更难发现:结果看着仍然合理。
→ 模式改为不带括号。
**③ looksLikeExternal 用黑名单 ⇒ 纯读标准库调用全被判"可能写"**
Marshal / ReadAll / NewRequest / NewReader 都不带写操作迹象,却被判写,
120 个工具里 119 个判成 SERIAL —— 等于工具没在工作,却**看上去在工作**
(保守方向不会引起怀疑)。
→ 改为 knownReadOnlyCalls 白名单。判定原则:**默认怀疑,明确信任** ——
写不动的东西要逐个列出来。
**④ 工具名提取打印 AST 内部结构**
`fmt.Sprintf("%v", a.Y)` 输出 `tp&{10500 10515 STRING "manage_social"}`,
名字里混着指针地址,人没法核对。
→ 改用 BasicLit.Value + 归一化去掉运行期前缀。
## annotate_parallel 的插入点定位
改文本匹配改结构体字面量这条路走了三次弯:
- 正则找"最后一个顶层字段"被嵌套 map 里的同形文本骗到,823 处重排改坏文件
- 变量前缀匹配没要求 RegisterTool( 在**同一行**,命中函数体里散落的字面量,
起点错到 switch case 中间,报错指向一处看起来完全无辜的分支
- 只按行末**净**深度判断:Parameters 写在单行时(进出同一行,净变化 0)
永远察觉不到曾进入深度 3,追踪一路跑到 1305 行才"收敛"
→ 最终按**行内峰值深度** + 记录进入深度 3 的行号判定。
## 幂等
未标注过的工具重复跑会插入第二份 ParallelSafe(duplicate field 编译错误)。
→ 加 blockHasDecl 预检。
This commit is contained in:
@ -51,6 +51,14 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, " 跳过 %s: %v\n", j.tool, err)
|
||||
continue
|
||||
}
|
||||
// 幂等:块内已有并发声明就跳过。
|
||||
//
|
||||
// 不加这条时,重跑会在已标注的工具上**再插一份** —— 而
|
||||
// duplicate field name 是编译期错误,跨文件批量跑时定位成本很高。
|
||||
if blockHasDecl(lines, j.tool) {
|
||||
fmt.Printf(" %s 已有声明,跳过\n", j.tool)
|
||||
continue
|
||||
}
|
||||
field := "ParallelSafe: true,"
|
||||
if j.kind == "serial" {
|
||||
field = "Serial: true,"
|
||||
@ -67,7 +75,12 @@ func main() {
|
||||
|
||||
// findInsertPoint 找到该 RegisterTool 字面量中,Parameters 闭合之后的位置。
|
||||
func findInsertPoint(lines []string, tool string) (int, error) {
|
||||
// 匹配两种注册形式:
|
||||
// RegisterTool("get_article", ...) 字面量
|
||||
// RegisterTool(tp+"get_article", ...) 变量前缀 + 字面量
|
||||
// 只认字面量会漏掉后者 —— example 里绝大多数是变量前缀形式。
|
||||
head := fmt.Sprintf(`RegisterTool("%s"`, tool)
|
||||
alt := fmt.Sprintf(`+"%s"`, tool)
|
||||
start := -1
|
||||
for i, l := range lines {
|
||||
if strings.Contains(l, head) {
|
||||
@ -75,6 +88,23 @@ func findInsertPoint(lines []string, tool string) (int, error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
// ★ 必须 RegisterTool( 与字面量在**同一行**。
|
||||
//
|
||||
// 我第一版只找含 `+"name"` 的行,命中了函数体里的散落字面量
|
||||
// (vanblog 的 handleAuth 里满是 "restore"/"update" 这类 case 分支),
|
||||
// 起点错到函数体中间,深度追踪再也回不到 2 ⇒ 插入点落在 1300+ 行,
|
||||
// 把文件改坏。
|
||||
//
|
||||
// 症状离原因很远:报错说"expected 1 expression",指向的是一处
|
||||
// 看起来完全正常的 case 分支。
|
||||
for i, l := range lines {
|
||||
if strings.Contains(l, alt) && strings.Contains(l, "RegisterTool(") {
|
||||
start = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
return 0, fmt.Errorf("找不到 RegisterTool(%q)", tool)
|
||||
}
|
||||
@ -85,6 +115,11 @@ func findInsertPoint(lines []string, tool string) (int, error) {
|
||||
inStr := false
|
||||
esc := false
|
||||
for i := start; i < len(lines); i++ {
|
||||
// peak = 本行内的峰值深度。
|
||||
//
|
||||
// 每行重置:它表示"这一行曾深入到多深",不是全程最大值 ——
|
||||
// 全程最大值一旦到过 3 就永远是 3,"曾进入 Parameters"判据随之失效。
|
||||
peak := depth
|
||||
for _, ch := range lines[i] {
|
||||
if esc {
|
||||
esc = false
|
||||
@ -105,6 +140,9 @@ func findInsertPoint(lines []string, tool string) (int, error) {
|
||||
case '(', '{', '[':
|
||||
depth++
|
||||
started = true
|
||||
if depth > peak {
|
||||
peak = depth
|
||||
}
|
||||
case ')', '}', ']':
|
||||
depth--
|
||||
}
|
||||
@ -133,16 +171,53 @@ func findInsertPoint(lines []string, tool string) (int, error) {
|
||||
// 2. Name:/Description: 本来就在深度 2,早于 Parameters。
|
||||
// 只判 depth==2 会在 Name 行就返回,插入点跑到 RegisterTool 之前,
|
||||
// 编译报 "expected 1 expression"。
|
||||
if started && depth >= 3 {
|
||||
// 判定"进入过 Parameters"要按**行内峰值深度**,不能只看行末净深度。
|
||||
//
|
||||
// get_meta 的 Parameters 全在一行:
|
||||
// Parameters: map[string]interface{}{"type":"object","properties":map[string]interface{}{}},
|
||||
// 这行净深度变化是 0(进去又出来)—— 只看净深就永远察觉不到曾进入
|
||||
// 深度 3 ⇒ 追踪一路跑到 1305 行才"收敛",插入点落在某个 case 分支
|
||||
// 中间,文件改坏。症状离原因很远:报错指向一处看起来完全正常的
|
||||
// switch case。
|
||||
if started && peak >= 3 {
|
||||
enteredAt = i
|
||||
}
|
||||
if enteredAt >= 0 && i > enteredAt && depth <= 2 {
|
||||
// 闭合判定:进入过 Parameters(enteredAt)之后,深度回到 2 的那一行
|
||||
// **就是** Parameters 的闭合行;插入点取它的**下一行**。
|
||||
//
|
||||
// ★ 不能要求 i > enteredAt:Parameters 写在单行时(get_meta 就是)
|
||||
// enteredAt 与闭合行是**同一行**,加上这个条件会跳到再下一行,
|
||||
// 插到 log.Printf 之前 —— 不报错,但声明落在了字面量外面。
|
||||
if enteredAt >= 0 && i >= enteredAt && depth <= 2 {
|
||||
return i + 1, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("括号深度追踪未收敛")
|
||||
}
|
||||
|
||||
// blockHasDecl 报告该工具的字面量里是否已有并发声明。
|
||||
func blockHasDecl(lines []string, tool string) bool {
|
||||
head := fmt.Sprintf(`RegisterTool("%s"`, tool)
|
||||
alt := fmt.Sprintf(`+"%s"`, tool)
|
||||
start := -1
|
||||
for i, l := range lines {
|
||||
if strings.Contains(l, head) || (strings.Contains(l, alt) && strings.Contains(l, "RegisterTool(")) {
|
||||
start = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if start < 0 {
|
||||
return false
|
||||
}
|
||||
// 从注册行往后找 30 行(工具定义不会更长)
|
||||
for i := start; i < len(lines) && i <= start+30; i++ {
|
||||
if strings.Contains(lines[i], "ParallelSafe:") || strings.Contains(lines[i], "Serial:") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func readLines(p string) []string {
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user