mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-02 23:37:24 +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 包全绿
214 lines
6.5 KiB
Go
214 lines
6.5 KiB
Go
package security
|
||
|
||
// 主体(principal)授权模型:**声明式**能力授予。
|
||
//
|
||
// ## 为什么需要它(用户 2026-10-26 指出的现状)
|
||
//
|
||
// QQ 插件现在用 stage 管道自己实现了一套权限模型(`beforeToolcall`):
|
||
// 判身份 → 查用户白名单 → 查"硬私有"工具 → 内部委派放行 → 写拒绝消息。
|
||
// 那是**在插件里执行安全策略**,后果实测有三类:
|
||
//
|
||
// ① 每个插件各写一套,判定必然漂移(QQ 拦了、其他入口没拦)
|
||
// ② 策略与被约束者在同一个进程里 —— 插件崩了/被替换,策略就没了
|
||
// ③ 拒绝语义散落(写 denial / 返回 error 各有各的写法)
|
||
//
|
||
// 正确形态:插件**声明**策略(谁可以调什么),内核**执行**策略。
|
||
//
|
||
// ## 与本文件已有内容的关系
|
||
//
|
||
// Policy 全局静态策略(黑名单/白名单/沙箱)
|
||
// ScopeOptions 轮次作用域(临时收紧)
|
||
// PrincipalPolicy(本文件)**按主体**授予的能力集合
|
||
//
|
||
// 判定顺序始终是:全局 deny → 主体 deny → 轮次 deny → 白名单 → 放行。
|
||
|
||
import (
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
// Principal 是一次调用的**主体**:谁在请求。
|
||
type Principal struct {
|
||
// ID 是主体标识(QQ 用户 id、群 id、驻留子 id、内部标记)。
|
||
ID string
|
||
// Kind 是主体类别("user" / "group" / "internal" / "resident" …)。
|
||
//
|
||
// ★ 为什么不只看 ID:内部委派与用户请求的 ID 可能相同
|
||
//(驻留子继承父的输入事件),但**权限模型完全不同** ——
|
||
// 这正是 QQ 那条 `isInternalToolCall` 分支存在的原因。
|
||
Kind string
|
||
// Tags 是附加标签(如 "admin"、"trusted"),策略可按标签授予。
|
||
Tags []string
|
||
}
|
||
|
||
// IsInternal 报告是否内部主体(agent 体系自己发起)。
|
||
func (p Principal) IsInternal() bool {
|
||
return p.Kind == "internal" || p.Kind == "resident"
|
||
}
|
||
|
||
// PrincipalPolicy 是某个主体的能力策略。
|
||
type PrincipalPolicy struct {
|
||
// AllowTools 是该主体**可用**的工具模式(空 = 不额外限制)。
|
||
AllowTools []string
|
||
// DenyTools 是该主体**禁用**的工具模式(优先于 AllowTools)。
|
||
DenyTools []string
|
||
// HardPrivateTools 是**永不外借**的工具模式(如 calendar_ / email_ / memory_)。
|
||
//
|
||
// ★ 与 DenyTools 的区别:DenyTools 是"这个主体不能用",
|
||
// HardPrivateTools 是"没有任何用户主体能用" —— 它防的是
|
||
// 用户偷看私人数据,而内部执行体(父的委派)本就有完整权限。
|
||
HardPrivateTools []string
|
||
}
|
||
|
||
// principalRegistry 持有各主体的策略(进程级,内核持有)。
|
||
type principalRegistry struct {
|
||
mu sync.RWMutex
|
||
byID map[string]PrincipalPolicy
|
||
byTag map[string]PrincipalPolicy
|
||
}
|
||
|
||
func newPrincipalRegistry() *principalRegistry {
|
||
return &principalRegistry{
|
||
byID: map[string]PrincipalPolicy{},
|
||
byTag: map[string]PrincipalPolicy{},
|
||
}
|
||
}
|
||
|
||
// SetByID 按主体 ID 声明策略。
|
||
func (r *principalRegistry) SetByID(id string, p PrincipalPolicy) {
|
||
if r == nil || strings.TrimSpace(id) == "" {
|
||
return
|
||
}
|
||
r.mu.Lock()
|
||
r.byID[id] = p
|
||
r.mu.Unlock()
|
||
}
|
||
|
||
// SetByTag 按标签声明策略(比逐 ID 声明更好维护)。
|
||
func (r *principalRegistry) SetByTag(tag string, p PrincipalPolicy) {
|
||
if r == nil || strings.TrimSpace(tag) == "" {
|
||
return
|
||
}
|
||
r.mu.Lock()
|
||
r.byTag[tag] = p
|
||
r.mu.Unlock()
|
||
}
|
||
|
||
// lookup 取出与主体相关的全部策略(ID + 每个标签)。
|
||
func (r *principalRegistry) lookup(p Principal) []PrincipalPolicy {
|
||
if r == nil {
|
||
return nil
|
||
}
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
var out []PrincipalPolicy
|
||
if pol, ok := r.byID[p.ID]; ok {
|
||
out = append(out, pol)
|
||
}
|
||
for _, tag := range p.Tags {
|
||
if pol, ok := r.byTag[tag]; ok {
|
||
out = append(out, pol)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// DeclarePrincipalPolicy 是**插件声明主体策略**的入口(SDK 转发到这里)。
|
||
//
|
||
// scope 语义:
|
||
//
|
||
// "id" → 按主体 ID(一个具体的人/群/子 agent)
|
||
// "tag" → 按标签(一类主体,如 "external")
|
||
func (r *Runtime) DeclarePrincipalPolicy(scope, key string, p PrincipalPolicy) error {
|
||
if r == nil || r.principals == nil {
|
||
return errNoRuntime
|
||
}
|
||
switch scope {
|
||
case "id":
|
||
r.principals.SetByID(key, p)
|
||
case "tag":
|
||
r.principals.SetByTag(key, p)
|
||
default:
|
||
return &policyErr{"security: principal scope must be \"id\" or \"tag\", got " + scope}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CheckToolFor 按**主体**判定工具是否可用。
|
||
//
|
||
// ★ 这是替代"插件在 stage 里自己拦"的入口:插件只声明策略,
|
||
// 判定与拒绝文案由内核统一产出(模型看到一致的拒绝语义)。
|
||
func (r *Runtime) CheckToolFor(p Principal, tool string) Decision {
|
||
if r == nil {
|
||
return allowed()
|
||
}
|
||
// ① 全局策略(deny 优先)。
|
||
if d := r.CheckTool(tool); !d.Allowed {
|
||
return d
|
||
}
|
||
pols := r.principals.lookup(p)
|
||
if len(pols) == 0 {
|
||
return allowed()
|
||
}
|
||
|
||
// ② 硬私有:任何**非内部**主体都不能用。
|
||
// 内部执行体(父的委派)本就有完整权限 —— 再拦一次等于
|
||
// "机器人自己派出去的活被自己拦住"(QQ 的实测教训)。
|
||
for _, pol := range pols {
|
||
if p.IsInternal() {
|
||
break
|
||
}
|
||
for _, pat := range pol.HardPrivateTools {
|
||
if matchTool(pat, tool) {
|
||
return deny("tool %q is hard-private and unavailable to %s principals", tool, p.Kind)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ③ 主体 deny。
|
||
for _, pol := range pols {
|
||
for _, pat := range pol.DenyTools {
|
||
if matchTool(pat, tool) {
|
||
return deny("tool %q is not permitted for this principal (deny %q)", tool, pat)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ④ 主体 allow:只要求"至少有一个声明了 allow 的策略命中"。
|
||
// 若所有策略都没声明 allow,则不做额外限制(沿用全局)。
|
||
anyAllowDeclared := false
|
||
allowHit := false
|
||
for _, pol := range pols {
|
||
if len(pol.AllowTools) == 0 {
|
||
continue
|
||
}
|
||
anyAllowDeclared = true
|
||
for _, pat := range pol.AllowTools {
|
||
if matchTool(pat, tool) {
|
||
allowHit = true
|
||
}
|
||
}
|
||
}
|
||
if anyAllowDeclared && !allowHit {
|
||
return deny("tool %q is outside this principal's allowed set", tool)
|
||
}
|
||
return allowed()
|
||
}
|
||
|
||
// PrincipalPolicies 返回已声明策略数量(状态面用)。
|
||
func (r *Runtime) PrincipalPolicies() int {
|
||
if r == nil || r.principals == nil {
|
||
return 0
|
||
}
|
||
r.principals.mu.RLock()
|
||
defer r.principals.mu.RUnlock()
|
||
return len(r.principals.byID) + len(r.principals.byTag)
|
||
}
|
||
|
||
// errNoRuntime 表示安全运行时未装配。
|
||
var errNoRuntime = &policyErr{"security: runtime is not initialized"}
|
||
|
||
type policyErr struct{ msg string }
|
||
|
||
func (e *policyErr) Error() string { return e.msg }
|