mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
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 包全绿
132 lines
5.0 KiB
Go
132 lines
5.0 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
agentAPI "github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||
)
|
||
|
||
// 设计口径:输出是 agent 的**主动调用** —— 收到一次输入后,可以往任意(已授权的)
|
||
// 通道发**任意多次**(分段播报、先回执后结论、同时通知多个通道都合法)。
|
||
//
|
||
// 这条判据钉住的是"提示词里不得出现输出次数限制"。此前 `tooldefs.go` 里写着
|
||
// 「每轮对话通常只需调用一次 output_send__{通道名} 即可完成回复」—— 一条凭空的限制,
|
||
// 会让模型自己收起合理的多次输出(用户现场指出)。
|
||
func TestSystemPromptDoesNotRestrictOutputCount(t *testing.T) {
|
||
parent, _, _ := newRootForResidents(t)
|
||
defer parent.Stop()
|
||
|
||
prompt := parent.buildSystemPrompt("", "你好")
|
||
banned := []string{
|
||
"only call once",
|
||
"call exactly once",
|
||
"usually only need to call",
|
||
"do not resend",
|
||
}
|
||
for _, b := range banned {
|
||
if strings.Contains(prompt, b) {
|
||
t.Fatalf("系统提示词里仍有输出次数限制 %q —— 设计上次数不限", b)
|
||
}
|
||
}
|
||
if !strings.Contains(prompt, "You choose how many outputs and which channels") {
|
||
t.Fatal("系统提示词应明确「输出次数与目标通道由你自己决定」")
|
||
}
|
||
if !strings.Contains(prompt, "No \"one output per turn\" limit") {
|
||
t.Fatal("系统提示词应显式否认「一轮只能发一次」")
|
||
}
|
||
}
|
||
|
||
// 「必须显式调用 outputch 才会送达用户」必须写在提示词里(用户 2026-10-22 明确要求)。
|
||
//
|
||
// 模型最常见的错误不是不会调工具,而是**以为返回文本就等于回复了**——
|
||
// 在异步通道上那等于自言自语,用户永远收不到。
|
||
//
|
||
// 变异:删掉这句 ⇒ 红。改成只在「异步通道」那一条里提 ⇒ 红(模型会漏读分项规则)。
|
||
func TestSystemPromptRequiresExplicitOutputCall(t *testing.T) {
|
||
parent, _, _ := newRootForResidents(t)
|
||
defer parent.Stop()
|
||
|
||
prompt := parent.buildSystemPrompt("", "你好")
|
||
|
||
// 必须是**开篇总述**级别的一句,不能只藏在分项里。
|
||
if !strings.Contains(prompt, "Content reaches the user only when you explicitly call `output_send__{channel}`") {
|
||
t.Fatalf("系统提示词必须以总述句明确「只有显式调用 outputch 才会送达用户」,当前:\n%s",
|
||
prompt[:min(600, len(prompt))])
|
||
}
|
||
// 且必须点明"返回纯文本用户看不到",否则模型仍会以为直接回答就行。
|
||
if !strings.Contains(prompt, "Returning plain text only speaks inside this reasoning step") {
|
||
t.Error("应显式说明「返回纯文本用户看不到」")
|
||
}
|
||
// 同步通道是唯一的例外,必须说清,否则模型在 webui 上也会多余地调工具。
|
||
if !strings.Contains(prompt, "The only exception") {
|
||
t.Error("同步通道例外必须显式标为「唯一的例外」")
|
||
}
|
||
// 批量能力要让模型知道(一 call 多发是本次内核新增能力)。
|
||
if !strings.Contains(prompt, "pass an array as payload") {
|
||
t.Error("提示词应告知 payload 可传数组一次发多条")
|
||
}
|
||
}
|
||
|
||
// 输出工具的 type 可省略,缺省按 text 处理(判据该拦的是"不知道发什么",
|
||
// 不是"没写众所周知的默认值")。
|
||
func TestOutputSendTypeDefaultsToText(t *testing.T) {
|
||
parent, _, _ := newRootForResidents(t)
|
||
defer parent.Stop()
|
||
|
||
dev := &outputTestDevice{name: "fakeout"}
|
||
if err := parent.io.RegisterDevice(dev); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
out := parent.executeOutputSendTool(agentAPI.ToolCall{
|
||
Name: "output_send__fakeout",
|
||
Arguments: map[string]interface{}{"payload": "只给 payload,不给 type"},
|
||
})
|
||
if out != "ok" {
|
||
t.Fatalf("省略 type 时应默认 text 并发送成功,得到 %q", out)
|
||
}
|
||
if len(dev.sent) != 1 {
|
||
t.Fatalf("通道应收到 1 次输出,得到 %d", len(dev.sent))
|
||
}
|
||
if args, _ := dev.sent[0]["args"].(map[string]interface{}); args["type"] != "text" {
|
||
t.Fatalf("缺省类型应为 text,实际 %v", args["type"])
|
||
}
|
||
|
||
// 工具 schema:required 只应含 payload
|
||
var found bool
|
||
for _, td := range parent.buildToolDefs() {
|
||
entry, _ := td.(map[string]interface{})
|
||
fn, _ := entry["function"].(map[string]interface{})
|
||
if n, _ := fn["name"].(string); n != "output_send__fakeout" {
|
||
continue
|
||
}
|
||
found = true
|
||
params, _ := fn["parameters"].(map[string]interface{})
|
||
req, _ := params["required"].([]string)
|
||
if len(req) != 1 || req[0] != "payload" {
|
||
t.Fatalf("output_send 的 required 应只有 payload,实际 %v", req)
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatal("未生成 output_send__fakeout 工具")
|
||
}
|
||
}
|
||
|
||
// 空 payload 仍应被拦(这条判据是对的:不知道发什么不能放过)。
|
||
func TestOutputSendStillRequiresPayload(t *testing.T) {
|
||
parent, _, _ := newRootForResidents(t)
|
||
defer parent.Stop()
|
||
|
||
if err := parent.io.RegisterDevice(&outputTestDevice{name: "fakeout"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out := parent.executeOutputSendTool(agentAPI.ToolCall{
|
||
Name: "output_send__fakeout",
|
||
Arguments: map[string]interface{}{"type": "text"},
|
||
})
|
||
if !strings.Contains(out, "payload 不能为空") {
|
||
t.Fatalf("空 payload 应被拦,得到 %q", out)
|
||
}
|
||
}
|