Files
HomeAgent/internal/agent/core/toolerror.go
JianFeeeee 55fa2685ae feat(security,device): security 成为独立内核模块(闸门+策略+限式),device 接管内置工具路由
security(新模块 internal/kernelmod/security)
- 凭据闸门:出向拦截成 @{别名}、入向清洗、执行前回填(internal/security/{gate,runtime}.go)
- 工具黑白名单 + 沙箱(读写根、网络、.. 逃逸):internal/security/policy.go
- 敏感词过滤 + 提示词注入预警(Finding 不带原文):internal/security/content.go
- 按主体授权(hard-private 对非内部主体永久拒绝):internal/security/principal.go
- 插件命名限式(可选档位,插件声明/内核执行,只能收窄):internal/security/profile.go
- 启动装配从 cmd/homed 迁入模块;启动顺序判据改盯真实装配点

device(新模块)
- 正式内核模块,Provides FacDevice,KernelState.Device
- 内置工具路由表下沉到 internal/toolroute(避免 core↔kernelboot 环),
  device 保留对外入口;agent 未挂载时退化本地 switch
- Agent 侧只留 api.ToolHost/DeviceSurface 适配层

SDK
- InjectOptions.SecurityProfile:插件按 inputch 声明本轮安全档位(可选)
- InjectOptions.TurnHint:插件注入**本轮事实**,内核以独立 system 消息注入,
  不混进用户正文(人设负责怎么说话,TurnHint 只放这一轮成立的事实)
- FacilitySecurity / FacilityDevice

提示词
- 内核自带 prompt/skill/工具返回全面英文化(中文提示会让模型行为漂移)
- 机制解释下沉到 4 个内置 skill(memory/output/tool-order/plugins),
  skill 描述压到 80 runes 内(原来 94–108 全被索引截断)
- 默认人格模板英文化;[Voice] 段删除,规则并入 persona 块(单一段落)
- 历史记忆工具改名 coldcontext_query/commit,图记忆语义改称 stable memory

修复
- tracker: exec mount → unix.Mount(降权运行 + AmbientCapabilities 下才能挂)、
  区分 EPERM 与不支持;启动日志说明后果与恢复方式
- healthcheck_kernel 输出瘦身:status.Tools 只给摘要(原 98,989 tokens/151% 预算)
- plugin_list 描述讲清 auto_start(按需)与 alive(此刻是否在跑),
  明确不能由 alive=false 推断能力不存在
- pluginmgr/webui 去掉写死的 /home/newqqagent 回退路径(env → pluginDir 反推 → 报错)
- lua: 盘上适配器与内置版本不一致且无基线时响亮告警(deepseek 流式丢失
  tool_calls 导致'模型不调工具'的真因)

测试:go build ./... 通过;go test ./... 60 包全绿
2026-10-26 20:55:25 +08:00

153 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"encoding/json"
"fmt"
"strings"
sdk "github.com/JianFeeeee/HologramAgent/internal/sdk"
)
// 工具失败原因码(与 SDK 的 ToolError.Reason 对应)。
const (
ErrReasonRequired = "required"
ErrReasonType = "type"
ErrReasonUnauthorized = "unauthorized"
ErrReasonTimeout = "timeout"
ErrReasonNotFound = "not_found"
)
// newToolError 构造一个结构化失败。
func newToolError(reason, field, detail, hint string) *sdk.ToolError {
return &sdk.ToolError{Reason: reason, Field: field, Detail: detail, Hint: hint}
}
// isToolError 报告一个工具返回值是否表示**失败**。
//
// 存在的理由:工具失败是以 `nil` error + 错误**值**返回的,而
// ToolResult.Success 此前被硬编码为 true(唯一赋值点),该字段恒真、
// 结构上不可能为 false。
//
// ⚠️ 判据必须兼容**既有三种约定**(实测于仓内,否则升级会把存量插件
// 的成功误判成失败——这是本函数最大的回归风险):
//
// ① {"error": msg} pluginmgr、cmd 的参数校验
// ② {"isError": true, "content": msg} files / clawhubadapter 的 errorResult
// ③ *sdk.ToolError 新写的工具(可选,不是迁移要求)
//
// 明确**不**作为失败判据的:
// - exit_code != 0:命令跑了但返回非零,属业务结果且带真实 stdout/stderr,
// 整条判失败会误伤「命令可用但结果非零」这类正常场景。
// - stderr 非空:cmd 成功路径常带 stderr(如 warn: deprecated)。
// - 字符串 / 数字 / bool / 数组 / nil:均视为成功(output_send 成功即返回 "ok")。
func isToolError(v interface{}) bool {
switch x := v.(type) {
case nil:
return false
case *sdk.ToolError:
return x != nil
case sdk.ToolError:
return true
case error:
// 工具显式返回 error —— 失败。
return x != nil
case map[string]interface{}:
// ② isError 优先:显式布尔标记,语义最明确。
if b, ok := x["isError"].(bool); ok && b {
return true
}
// ① error 键:非空字符串才算失败。
if e, ok := x["error"]; ok {
switch ev := e.(type) {
case string:
return strings.TrimSpace(ev) != ""
case nil:
return false
default:
// error 是结构化值(如嵌套的 ToolError)—— 视为失败。
return true
}
}
return false
case string:
// 自由文本无法可靠判别成败,按**成功**处理(保守:宁可少报失败,
// 也不要把正常结果报成失败)。失败请用上面三种显式形态。
return false
default:
return false
}
}
// toolErrorText renders a failed tool result for the model.
// A structured ToolError includes its Hint so the model can act on the real cause.
func toolErrorText(toolName string, v interface{}) string {
switch x := v.(type) {
case *sdk.ToolError:
return renderToolError(toolName, x)
case sdk.ToolError:
return renderToolError(toolName, &x)
case map[string]interface{}:
if b, ok := x["isError"].(bool); ok && b {
msg, _ := x["content"].(string)
if msg == "" {
msg = "(plugin did not provide a reason)"
}
return fmt.Sprintf("Tool %s failed: %s", toolName, msg)
}
if e, ok := x["error"]; ok {
if s, ok := e.(string); ok {
return fmt.Sprintf("Tool %s failed: %s", toolName, s)
}
}
case error:
return fmt.Sprintf("Tool %s failed: %v", toolName, x)
}
return fmt.Sprintf("Tool %s failed: %v", toolName, v)
}
// renderToolError renders a structured failure with field/reason/hint,
// so the model knows what to change instead of seeing only "failed".
func renderToolError(toolName string, e *sdk.ToolError) string {
var sb strings.Builder
fmt.Fprintf(&sb, "Tool %s failed", toolName)
if e.Field != "" {
fmt.Fprintf(&sb, " (field %s)", e.Field)
}
sb.WriteString(": ")
if e.Reason != "" {
sb.WriteString(e.Reason)
}
if e.Detail != "" {
sb.WriteString(" — ")
sb.WriteString(e.Detail)
}
if e.Hint != "" {
sb.WriteString("\nFix this and retry: ")
sb.WriteString(e.Hint)
}
return sb.String()
}
// renderToolResult renders tool output for the model.
// Structured failures take priority and include actionable information;
// successful non-string values use compact JSON instead of Go map syntax.
func renderToolResult(toolName string, raw interface{}) string {
if raw == nil {
return ""
}
if isToolError(raw) {
return toolErrorText(toolName, raw)
}
switch x := raw.(type) {
case string:
return x
case error:
return fmt.Sprintf("%v", x)
}
// 非字符串:紧凑 JSON。
if b, err := json.Marshal(raw); err == nil {
return string(b)
}
return fmt.Sprintf("%v", raw)
}