Files
HomeAgent/internal/security/gate.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

145 lines
5.2 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 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
}