mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
feat(security): 插件按工具声明内容策略 + 目录名统一
## 一、插件声明层(用户裁定)
> 「敏感词策略也应该是插件声明啊,每个插件声明自己轮次以及自己工具的
> 敏感词策略」
SecurityProfile 新增两个字段:
ToolSensitiveWords map[string][]string // key=工具名模式(尾 *),value=词表
DetectInjection bool // 开启内核内置注入迹象识别
语义是**追加不覆盖**:整轮 SensitiveWords 仍然生效。
取宽不取窄的理由:当前处置是 B 档(标记+警示,不阻断),
多标一条只是多一句警示,漏标则是安全缺口。
## 二、修掉我自己造的「莲藕式分层」
前一版把 *security.Round 从 runInputTask 一路搬到工具链
(prepareInputTask(evt, round) → TaskFrame.SecRound →
executeToolCallOutcomeWithRound),并在 rtchild 里另补一条。
那是把**分层策略实现成跨层传参**:每多一条工具执行路径就要求调用方
多传一次句柄。profileActivator 的设计注释早就写明了正路:
> 工具调用发生在 LLM 决定之后,此时插件已不在调用栈上。
> 唯一可靠的传递方式是**内核在输入进来时记住**,在该轮的工具判定里使用。
改为 security 自取:
Runtime.ScanContentForAgent(agentID, isChild, ins, text)
→ ForAgent(agentID, isChild) // per-agent 视图,天然不串档
→ EffectiveProfile() // session > longtime
→ 合并 全局词表 + 档位词表 + 工具词表 + DetectInjection
core 只递交**事实**(我是谁 / 哪个工具 / 什么文本),不认识 Round。
与 CheckToolForAgent 同一套身份推断,不引入第二个入口语义。
TaskFrame.SecRound / executeToolCallOutcomeWithRound 全部撤除。
## 三、目录名统一
third_party/homeagent-sdk → third_party/hologramagentsdk
module 与远端仓早已是 hologramagentsdk,只剩物理目录是遗留名。
同步改了 go.mod replace、构建/打包/部署脚本、5 处测试里的分段路径。
注意:这里改的是**主仓 vendored 的 SDK 源码路径**,example/ 由 SDK
独立仓自己跟踪,主仓不涉及。
## 判据
TestToolSensitiveWordsAppendOnlyForThatTool 词表只对目标工具生效 + 追加语义
TestToolSensitiveWordsUseAgentScope security 按 agent scope 自取生效
(变异验证:删掉 ForAgent 查询 → 红)
## 验证
go build ./... → 0
go vet ./... → 0
go test ./... -count=1 -race → 62 ok / 0 FAIL / 0 DATA RACE
gofmt -l internal/ → 干净
This commit is contained in:
@ -1 +0,0 @@
|
||||
../../../csrc/include/ha_codec.h
|
||||
97
internal/agent/api/ha_codec.h
Normal file
97
internal/agent/api/ha_codec.h
Normal file
@ -0,0 +1,97 @@
|
||||
#ifndef HA_CODEC_H
|
||||
#define HA_CODEC_H
|
||||
|
||||
/*
|
||||
* ha_codec — HomeAgent 内核编解码层(C 实现)
|
||||
*
|
||||
* ============================ 接口冻结声明 ============================
|
||||
* 本头文件是对外契约。函数签名、语义、返回值一经发布即为冻结接口,
|
||||
* 修改必须走大版本流程(与 third_party/hologramagentsdk 同一冻结标准)。
|
||||
*
|
||||
* 设计约束(见 docs/zh/c-core/llm-orchestration-c.md §四):
|
||||
* 1. 只吃 const char* + **显式长度**,出数值/字节偏移 —— 不回调 Go、
|
||||
* 不传 Go 指针、不要求 NUL 结尾
|
||||
* 2. **不 malloc**:不需要出参缓冲区,需要「结果」时返回字节偏移/长度,
|
||||
* 由调用方在自己的缓冲上切片(零拷贝)
|
||||
* 3. 无状态、纯函数、线程安全(不写全局可变状态)
|
||||
*
|
||||
* 当前覆盖:L1 协议编解码层中的纯计算部分(第一个最小切片)。
|
||||
*
|
||||
* ============================ 为什么签名带长度 ============================
|
||||
* 初版签名用 `const char*` 隐含「NUL 结尾」,于是每次调用都要:
|
||||
* Go `C.CString` 分配+拷贝一遍 → C `strlen` 再扫一遍。
|
||||
* 实测这部分开销占单次调用的 80% 以上(cgo 边界本身仅 ~30ns,
|
||||
* 而初版 ModelContextWindow 实测 175ns)。
|
||||
* 改为「指针 + 长度」后,Go 侧用 unsafe.StringData 直接传底层数组,
|
||||
* 零分配零拷贝。这是设计约束第 1 条的字面要求。
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "ha_abi.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ==================== ABI 自述(供 Go 侧与日志核对) ==================== */
|
||||
|
||||
/* 返回 HA_CODEC_ABI_VERSION(major*1000 + minor)。
|
||||
*
|
||||
* 存在的意义:Go 侧不该靠 `#include` 宏做版本断言(cgo 头文件里的宏在
|
||||
* 预处理后不可见),而要**运行期/测试期**能问 C 侧「你自称什么版本」。
|
||||
* 由 codec_cgo.go 绑定、codec_abiversion_test.go 与 C 侧宏三方比对。 */
|
||||
int ha_codec_abi_version(void);
|
||||
|
||||
/* ==================== 模型上下文窗口推断 ==================== */
|
||||
|
||||
/* 无法从模型名推断时的哨兵值(与 Go 侧一致)。
|
||||
*
|
||||
* 为什么返回哨兵而不是直接给兜底值:调用方需要区分「真推断出了」与
|
||||
* 「推断不出、只能兜底」——后者要打一行日志(窗口被低估必须可见),
|
||||
* 并提示部署方用 per-source context_window 显式声明。
|
||||
* 若 C 侧直接返回兜底值,调用方就永远分不清这两种情况。 */
|
||||
#define HA_CODEC_CONTEXT_WINDOW_UNKNOWN (-1)
|
||||
|
||||
/* 由模型名推断最大上下文窗口(token 数);推断不出返回
|
||||
* HA_CODEC_CONTEXT_WINDOW_UNKNOWN。
|
||||
*
|
||||
* model 为 UTF-8 字节序列,**不需要 NUL 结尾**;model_len 是字节数。
|
||||
* model 为 NULL 或 model_len 为 0 时返回 UNKNOWN。
|
||||
*
|
||||
* 匹配大小写不敏感(仅对 ASCII 字母做折叠;非 ASCII 字节按原样比较,
|
||||
* 与 Go 侧对模型名的实际输入一致)。
|
||||
*
|
||||
* 语义必须与 Go 侧 modelContextWindowPure 逐值一致(黄金对照测试钉死)。 */
|
||||
int ha_codec_model_context_window(const char *model, size_t model_len);
|
||||
|
||||
/* ==================== token 估算与截断 ==================== */
|
||||
|
||||
/* 粗略估算 token 数。
|
||||
*
|
||||
* 规则(与 Go 侧 EstimateTokens 一致):保守取 max(1, runeCount * 2)。
|
||||
* 按 UTF-8 **字符数**(rune)计,不是字节数。
|
||||
* text 为 NULL 或 text_len 为 0 返回 0。
|
||||
*
|
||||
* 非法 UTF-8 序列按 Go 的 utf8 解码语义处理(每字节一个 rune),
|
||||
* 保证与 Go 侧逐值一致。 */
|
||||
int ha_codec_estimate_tokens(const char *text, size_t text_len);
|
||||
|
||||
/* 按 token 预算计算「应保留的字节数」。
|
||||
*
|
||||
* ★ 返回的是**字节数**而非字符串:截断结果必然是输入的前缀,
|
||||
* 调用方直接在自己的缓冲上切片即可(零拷贝、无出参缓冲区、无 malloc)。
|
||||
*
|
||||
* 语义与 Go 侧 TruncateByTokens 一致:从开头保留 maxTokens/2 个 rune;
|
||||
* 未超预算时返回 text_len(即整串)。
|
||||
* max_tokens <= 0 或 text 为 NULL/text_len 为 0 时返回 0。
|
||||
*
|
||||
* 返回值保证 <= text_len。 */
|
||||
size_t ha_codec_truncate_by_tokens(const char *text, size_t text_len,
|
||||
int max_tokens);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* HA_CODEC_H */
|
||||
@ -7,7 +7,7 @@ package all
|
||||
// 设施名同时存在于**两处**,且必须逐字一致:
|
||||
//
|
||||
// 内核:internal/kernelmod/facilities.go (模块 Provides 用)
|
||||
// SDK:third_party/homeagent-sdk/sdk/facilities.go (插件声明用)
|
||||
// SDK:third_party/hologramagentsdk/sdk/facilities.go (插件声明用)
|
||||
//
|
||||
// 它们**不能合并**:SDK 是独立发布的模块(外部插件依赖它),
|
||||
// 而内核的 internal 规则又禁止内部包被外部 import。
|
||||
@ -67,7 +67,7 @@ func extractFacilities(t *testing.T, path string) map[string]string {
|
||||
func TestFacilityNamesMatchPublicSDK(t *testing.T) {
|
||||
kernel := extractFacilities(t, filepath.Join("..", "facilities.go"))
|
||||
sdk := extractFacilities(t, filepath.Join("..", "..", "..",
|
||||
"third_party", "homeagent-sdk", "sdk", "facilities.go"))
|
||||
"third_party", "hologramagentsdk", "sdk", "facilities.go"))
|
||||
|
||||
kernelVals := map[string]string{} // 值 → 内核常量名
|
||||
for name, v := range kernel {
|
||||
|
||||
@ -23,7 +23,7 @@ import (
|
||||
// 工作树里可能因为 vendoring 或本地试验而临时存在,那不算回归。
|
||||
|
||||
// vendoredSDKRoot 是 vendored SDK 在主仓里的位置。
|
||||
const vendoredSDKRoot = "third_party/homeagent-sdk"
|
||||
const vendoredSDKRoot = "third_party/hologramagentsdk"
|
||||
|
||||
// repoRoot 返回主仓根目录(本测试包在 internal/meta/,往上是两层)。
|
||||
func repoRoot(t *testing.T) string {
|
||||
@ -85,7 +85,7 @@ func TestVendoredSDKKeepsRequiredPackages(t *testing.T) {
|
||||
"meta", // 版本号:主仓与 SDK 版本一致性检查
|
||||
"meta/meta.go",
|
||||
// ★ 不列 tools/hmapdev/yaegi:.gitignore 第 33 行忽略了
|
||||
// third_party/homeagent-sdk/tools/,它从来没被跟踪过。
|
||||
// third_party/hologramagentsdk/tools/,它从来没被跟踪过。
|
||||
// 而 grep 到的 yaegi 引用是 **SDK 仓自己的文件**在互相 import
|
||||
// (tools/hmapdev/yaegi/interp.go → .../mocksdk),不是主仓依赖。
|
||||
// 我第一版把它当主仓依赖写进来,判据直接假红。
|
||||
|
||||
@ -23,7 +23,7 @@ package plugin
|
||||
// ## 来源
|
||||
//
|
||||
// 源码树里约定 `<插件源码目录>/skill/<skillName>/SKILL.md`(与 SDK 示例
|
||||
// third_party/homeagent-sdk/example/editdoc/skill/office-tools 一致)。
|
||||
// third_party/hologramagentsdk/example/editdoc/skill/office-tools 一致)。
|
||||
// 但内置插件是**编译期内置**,运行时没有源码树,所以 SKILL 正文必须
|
||||
// 编进二进制(见各插件包里的 embed),由插件通过 SkillProvider 暴露。
|
||||
//
|
||||
|
||||
@ -17,15 +17,15 @@ import (
|
||||
// 三份 sdk.lua(内核内嵌 / 工具链模板 / 项目副本)历史上各自漂移过,
|
||||
// 表现为「mock 里有的 API,内核运行时是 nil」这类静默失配。
|
||||
// 事实源是 SDK 仓的 sdk/lua/sdk.lua,内核副本由
|
||||
// third_party/homeagent-sdk/scripts/sync-lua-sdk.sh 同步。
|
||||
// third_party/hologramagentsdk/scripts/sync-lua-sdk.sh 同步。
|
||||
func TestLuaSDKMockSingleSource(t *testing.T) {
|
||||
canonical, err := os.ReadFile(filepath.Join("..", "..", "third_party", "homeagent-sdk", "sdk", "lua", "sdk.lua"))
|
||||
canonical, err := os.ReadFile(filepath.Join("..", "..", "third_party", "hologramagentsdk", "sdk", "lua", "sdk.lua"))
|
||||
if err != nil {
|
||||
t.Skipf("SDK repo canonical sdk.lua not available: %v", err)
|
||||
}
|
||||
if string(canonical) != luaSDK.SDKSource {
|
||||
t.Fatal("内核内嵌 sdk.lua 与 SDK 仓 sdk/lua/sdk.lua 不一致;" +
|
||||
"请跑 third_party/homeagent-sdk/scripts/sync-lua-sdk.sh")
|
||||
"请跑 third_party/hologramagentsdk/scripts/sync-lua-sdk.sh")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ func publishedPluginMgrMethods(t *testing.T, sdkPath string) []string {
|
||||
// 这挡住了"SDK 加了方法但忘记登记能力" ⇒ 那种方法会被按 CapCore 放行,
|
||||
// 或者直接"未知 method"。
|
||||
func TestBridgeCoversEveryPublishedPluginMgrMethod(t *testing.T) {
|
||||
sdkPath := filepath.Join("..", "..", "..", "third_party", "homeagent-sdk", "sdk", "plugin.go")
|
||||
sdkPath := filepath.Join("..", "..", "..", "third_party", "hologramagentsdk", "sdk", "plugin.go")
|
||||
methods := publishedPluginMgrMethods(t, sdkPath)
|
||||
|
||||
// SDK 方法名 → proc method 值(命名约定:StopPlugin → plugin.mgrStop)
|
||||
@ -160,16 +160,16 @@ func TestBridgeDispatchesEveryPluginMethod(t *testing.T) {
|
||||
// 漏 ⇒ 新生成的 plugin.bin **编译不过**(procPluginMgr 不满足接口)。
|
||||
// 这是最晚才发现的一环(编译期才报,且只在用户执行 hmapdev build 时暴露)。
|
||||
func TestHmapdevTemplateImplementsAllPluginMgrMethods(t *testing.T) {
|
||||
sdkPath := filepath.Join("..", "..", "..", "third_party", "homeagent-sdk", "sdk", "plugin.go")
|
||||
sdkPath := filepath.Join("..", "..", "..", "third_party", "hologramagentsdk", "sdk", "plugin.go")
|
||||
methods := publishedPluginMgrMethods(t, sdkPath)
|
||||
|
||||
tmplPath := filepath.Join("..", "..", "..", "third_party", "homeagent-sdk",
|
||||
tmplPath := filepath.Join("..", "..", "..", "third_party", "hologramagentsdk",
|
||||
"tools", "hmapdev", "templates", "proc_main.go.tmpl")
|
||||
src, err := os.ReadFile(tmplPath)
|
||||
if err != nil {
|
||||
// ★ 文件缺失必须 **skip 而不是 fail**(2026-10-17 CI 实测):
|
||||
//
|
||||
// `third_party/homeagent-sdk/tools/` 在 .gitignore 里(SDK 仓自治,
|
||||
// `third_party/hologramagentsdk/tools/` 在 .gitignore 里(SDK 仓自治,
|
||||
// 本仓不跟踪)。所以:
|
||||
// · 本机(完整 SDK 检出)有该文件 ⇒ 判据能跑
|
||||
// · CI(只 checkout 本仓)没有该文件 ⇒ 原本 Fatalf ⇒ **CI 必红**
|
||||
@ -178,7 +178,7 @@ func TestHmapdevTemplateImplementsAllPluginMgrMethods(t *testing.T) {
|
||||
// "Go build / vet / test" 和 "Build linux/amd64" 双红,把 PR 卡住。
|
||||
//
|
||||
// skip 不是放弃检查:跨仓一致性由 **SDK 仓自己的判据**守着
|
||||
// (third_party/homeagent-sdk/tools/hmapdev/scaffold_stdout_test.go
|
||||
// (third_party/hologramagentsdk/tools/hmapdev/scaffold_stdout_test.go
|
||||
// 等),那边是模板的真实归属地。本判据在本机开发时仍然生效。
|
||||
t.Skipf("跳过:SDK 仓的 tools/ 不在本仓跟踪范围(.gitignore),"+
|
||||
"跨仓一致性由 SDK 仓判据守护。缺文件: %v", err)
|
||||
|
||||
@ -125,10 +125,10 @@ func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
|
||||
// 工具链在 SDK 1.2.0 起改名 hmapdev(原 plugindev)。两个目录都接受:
|
||||
// 旧检出(软链或旧版 SDK 仓)仍能跑本测试,新检出走新路径。
|
||||
tmplDir := filepath.Join("..", "..", "..",
|
||||
"third_party", "homeagent-sdk", "tools", "hmapdev", "templates")
|
||||
"third_party", "hologramagentsdk", "tools", "hmapdev", "templates")
|
||||
if _, err := os.Stat(tmplDir); err != nil {
|
||||
tmplDir = filepath.Join("..", "..", "..",
|
||||
"third_party", "homeagent-sdk", "tools", "plugindev", "templates")
|
||||
"third_party", "hologramagentsdk", "tools", "plugindev", "templates")
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
@ -142,7 +142,7 @@ func buildPluginWithRealTemplate(t *testing.T, businessCode string) string {
|
||||
mustWriteFile(t, filepath.Join(dir, rt.out), string(data))
|
||||
}
|
||||
|
||||
sdkPath, err := filepath.Abs(filepath.Join("..", "..", "..", "third_party", "homeagent-sdk"))
|
||||
sdkPath, err := filepath.Abs(filepath.Join("..", "..", "..", "third_party", "hologramagentsdk"))
|
||||
if err != nil {
|
||||
t.Fatalf("解析 SDK 路径: %v", err)
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ package plugins
|
||||
//
|
||||
// ## 判定标准:只有「必须用内部 SDK 能力」的才留在本清单(2026-10-19)
|
||||
//
|
||||
// 内部 SDK(internal/sdk)比公开 SDK(third_party/homeagent-sdk/sdk)
|
||||
// 内部 SDK(internal/sdk)比公开 SDK(third_party/hologramagentsdk/sdk)
|
||||
// 多出一批**不对外授予**的能力面。外部插件用的是 proc 桥的收窄能力集,
|
||||
// 这些接口在类型层就不存在(见 internal/plugin/proc/capability.go 的
|
||||
// withheldCapabilities)。所以:
|
||||
|
||||
@ -71,7 +71,7 @@ func hostExecutableKind(path string) (string, bool) {
|
||||
// realPluginDir 返回某个 example 插件的 linux 产物路径。
|
||||
func realPluginBinary(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
root, err := filepath.Abs(filepath.Join("..", "..", "third_party", "homeagent-sdk", "example", name))
|
||||
root, err := filepath.Abs(filepath.Join("..", "..", "third_party", "hologramagentsdk", "example", name))
|
||||
if err != nil {
|
||||
t.Fatalf("解析插件目录: %v", err)
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@ import (
|
||||
// KnowledgeAPI 是内置插件使用的全量知识库接口。
|
||||
//
|
||||
// 为什么不把多模态加进 pubsdk.KnowledgeAPI:那会改动
|
||||
// third_party/homeagent-sdk/sdk/ 的公开契约,而发布纪律要求
|
||||
// `git diff main -- third_party/homeagent-sdk/sdk/` 恒为 0
|
||||
// third_party/hologramagentsdk/sdk/ 的公开契约,而发布纪律要求
|
||||
// `git diff main -- third_party/hologramagentsdk/sdk/` 恒为 0
|
||||
// (改动它等于一次 major bump + 完整的外部发版流程)。
|
||||
// internal/sdk 明确不受此约束("内部实现自由"),且这里本来就是
|
||||
// "内核侧接口 = 公共接口 + 内置插件额外能力" 的既有范式
|
||||
|
||||
@ -88,6 +88,7 @@ func (s *securityImpl) DeclareProfile(p pubsdk.SecurityProfile) error {
|
||||
},
|
||||
SensitiveWords: p.SensitiveWords,
|
||||
ToolSensitiveWords: p.ToolSensitiveWords,
|
||||
DetectInjection: p.DetectInjection,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("security: declare profile %q: %w", name, err)
|
||||
}
|
||||
|
||||
@ -52,6 +52,16 @@ type Profile struct {
|
||||
// key 是工具名模式(支持尾部 `*`),value 是该工具额外追加的词。
|
||||
// 语义**追加不覆盖** —— 见 SDK 侧同名字段的说明。
|
||||
ToolSensitiveWords map[string][]string
|
||||
// DetectInjection 让这一档开启内置的**提示词注入迹象**识别。
|
||||
//
|
||||
// 与 SensitiveWords 的分工:敏感词是插件给的**数据**(它知道自己的
|
||||
// 哪些词敏感),注入迹象是内核内置的**形态**规则(ignore previous /
|
||||
// 角色劫持 / 索要系统提示 / 伪造更高优先级)。
|
||||
//
|
||||
// ★ 默认 false:失败即静默。注入识别会命中"扮演""你现在是"这类
|
||||
// 正常对话词,对所有插件默认开启会让常识对话充满误报;
|
||||
// 由插件按自己的内容形态显式声明。
|
||||
DetectInjection bool
|
||||
// DisableNetwork 在这一档内禁止网络意图。
|
||||
DisableNetwork bool
|
||||
// Sandbox 覆盖沙箱约束(零值 = 沿用全局)。
|
||||
|
||||
@ -407,6 +407,10 @@ func (r *Runtime) effectiveContentForTool(profileName, toolName string) ContentP
|
||||
if ok && len(prof.SensitiveWords) > 0 {
|
||||
p.SensitiveWords = append(append([]string{}, p.SensitiveWords...), prof.SensitiveWords...)
|
||||
}
|
||||
// 注入迹象:档位可为本轮显式开启(默认 false,理由见 Profile.DetectInjection)。
|
||||
if ok && prof.DetectInjection {
|
||||
p.DetectInjection = true
|
||||
}
|
||||
// 工具级追加(在整轮词表之上)。toolName 为空或档位没声明该工具 ⇒ 零影响。
|
||||
if ok && toolName != "" && len(prof.ToolSensitiveWords) > 0 {
|
||||
for pattern, words := range prof.ToolSensitiveWords {
|
||||
|
||||
Reference in New Issue
Block a user