mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +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 包全绿
145 lines
5.2 KiB
Go
145 lines
5.2 KiB
Go
package security
|
||
|
||
// 安全闸门(Security Gate):**所有跨模块数据流的必经点**。
|
||
//
|
||
// ## 为什么它不是一个"调用方要记得调"的辅助函数
|
||
//
|
||
// 用户裁定(2026-10-26):
|
||
//
|
||
// > 安全模块不是独立的模块吗?core 发出的请求和 device 返回的请求都会
|
||
// > 经过安全模块审查,通过才能进行下一步 —— 相当于安全模块就是横在
|
||
// > 其他所有模块与 core 之间的一堵墙。
|
||
//
|
||
// 因此本文件定义的是**边界契约**:出向(Core → 外部)与入向(外部 → Core)
|
||
// 两个方向、各自一个入口。谁想跨过这条边界,就只能走这两个函数 ——
|
||
// 而不是各自决定"要不要顺手扫一下"。
|
||
//
|
||
// ## 实现形态:显式代理,不是运行时魔法
|
||
//
|
||
// 落地方式是**显式代理**(KernelState 里换掉公共挂载点,见
|
||
// internal/kernelmod/security)。这里只提供闸门本身:
|
||
//
|
||
// 出向:PrepareOutbound —— 拦截明文(换成 @{别名}),保留别名回填能力
|
||
// 入向:ScrubInbound —— 把返回内容里的明文换成 @{别名}
|
||
//
|
||
// ## 与 Store 的分工
|
||
//
|
||
// Store 是"变量库"(别名、加密、回填、替换)。
|
||
// Gate 是"边界"(方向、来源、放行/拒绝/改写、审计)。
|
||
// 上层只认 Gate;Store 的细节不外泄给业务模块。
|
||
|
||
import (
|
||
"log"
|
||
)
|
||
|
||
// Direction 是数据流方向。
|
||
type Direction string
|
||
|
||
const (
|
||
// DirectionOutbound 是 Core → 外部(工具参数、载荷、设备命令、插件调用)。
|
||
DirectionOutbound Direction = "outbound"
|
||
// DirectionInbound 是外部 → Core(工具结果、设备回执、事件载荷)。
|
||
DirectionInbound Direction = "inbound"
|
||
)
|
||
|
||
// Inspection 描述一次过境的数据属于谁、往哪去。
|
||
//
|
||
// ★ 只放**定位信息**,不放内容:内容走参数,避免这里成为明文副本。
|
||
type Inspection struct {
|
||
Direction Direction
|
||
// Stage 是过境点的短名("tool_args" / "tool_result" / "plugin_result" …),
|
||
// 用于审计与日志定位(不是给模型的文案)。
|
||
Stage string
|
||
// Tool / Channel / AgentID / Source 是定位信息,缺省时为空。
|
||
Tool string
|
||
Channel string
|
||
AgentID string
|
||
Source string
|
||
}
|
||
|
||
// where 返回审计用的位置串(形如 "tool_args:coldcontext_query")。
|
||
func (i Inspection) where() string {
|
||
w := i.Stage
|
||
if i.Tool != "" {
|
||
w += ":" + i.Tool
|
||
} else if i.Channel != "" {
|
||
w += ":" + i.Channel
|
||
}
|
||
return w
|
||
}
|
||
|
||
// Gate 是安全边界的**唯一入口**。
|
||
//
|
||
// 未启用安全模块时(Store == nil)两个方法都是**零成本透传**:
|
||
// 这不是"降级放行",而是"安全能力未装配"——配置关掉时用户明确选择不用它。
|
||
type Gate struct {
|
||
store *Store
|
||
}
|
||
|
||
// NewGate 构造闸门。store 为 nil 表示未启用(所有方法透传)。
|
||
func NewGate(store *Store) *Gate { return &Gate{store: store} }
|
||
|
||
// Store 返回底层变量库(只给 security 模块内部与显式需要元信息的工具用)。
|
||
func (g *Gate) Store() *Store {
|
||
if g == nil {
|
||
return nil
|
||
}
|
||
return g.store
|
||
}
|
||
|
||
// Enabled 报告闸门是否真的在拦。
|
||
func (g *Gate) Enabled() bool { return g != nil && g.store != nil }
|
||
|
||
// PrepareOutbound 处理 Core → 外部的文本。
|
||
//
|
||
// 语义:把**明文凭据**换成 `@{别名}` 交给外部,而不是把明文发出去。
|
||
// 外部若需要真实值,由 Fill* 在真正调用插件/设备前回填
|
||
// (对外部保持"它看到的是明文"的既有语义,对 LLM 保持"只有别名")。
|
||
//
|
||
// 返回 (改写后文本, 拦截事件数)。未启用时原样返回。
|
||
func (g *Gate) PrepareOutbound(ins Inspection, text string) (string, int) {
|
||
if !g.Enabled() || text == "" {
|
||
return text, 0
|
||
}
|
||
out, evts := g.store.Replace(text, ins.where(), Patterns)
|
||
for _, e := range evts {
|
||
log.Printf("[security] %s blocked %s (pattern %s, %d bytes, merged=%v)",
|
||
ins.where(), e.Alias, e.Kind, e.Len, e.Merged)
|
||
}
|
||
return out, len(evts)
|
||
}
|
||
|
||
// ScrubInbound 处理外部 → Core 的文本。
|
||
//
|
||
// ★ 为什么入向也必须扫:入向内容会直接进入模型上下文(role=tool)。
|
||
// 只在"写记忆"时清洗是**最坏的组合**——记忆侧干净、当轮泄漏、事后查不到
|
||
// (该缺陷在 agent/core 的 toolcall 里有实测记录)。
|
||
//
|
||
// 返回 (清洗后文本, 拦截事件数)。未启用时原样返回。
|
||
func (g *Gate) ScrubInbound(ins Inspection, text string) (string, int) {
|
||
if !g.Enabled() || text == "" {
|
||
return text, 0
|
||
}
|
||
out, evts := g.store.Replace(text, ins.where(), Patterns)
|
||
for _, e := range evts {
|
||
log.Printf("[security] %s scrubbed %s (pattern %s, %d bytes, merged=%v)",
|
||
ins.where(), e.Alias, e.Kind, e.Len, e.Merged)
|
||
}
|
||
return out, len(evts)
|
||
}
|
||
|
||
// FillForExecution 在**真正交给外部执行前**把 `@{别名}` 换回明文。
|
||
//
|
||
// 语义边界:别名只出现在 LLM 侧;外部(插件/设备)看到真实值且对替换无感知。
|
||
// 返回 (回填后文本, 回填处数)。
|
||
func (g *Gate) FillForExecution(ins Inspection, text string) (string, int) {
|
||
if !g.Enabled() || text == "" {
|
||
return text, 0
|
||
}
|
||
out, n, used := g.store.Fill(text)
|
||
if n > 0 {
|
||
log.Printf("[security] %s backfilled %d place(s) (aliases %v)", ins.where(), n, used)
|
||
}
|
||
return out, n
|
||
}
|