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 包全绿
This commit is contained in:
JianFeeeee
2026-10-26 20:55:25 +08:00
parent ca956f69bf
commit 55fa2685ae
92 changed files with 3932 additions and 596 deletions

View File

@ -0,0 +1,173 @@
// Package security 是 security 模块:**安全防火墙**(Secret Firewall + 边界闸门)。
//
// ## 它是什么(用户裁定 2026-10-26)
//
// > 安全模块不是独立的模块吗?core 发出的请求和 device 返回的请求都会
// > 经过安全模块审查,通过才能进行下一步,否则会被拦截 —— 相当于安全模块
// > 就是横在其他所有模块与 core 之间的一堵墙。
//
// 所以本模块不是"某项能力",而是**数据面边界**:
//
// Core ──出向审查──▶ 外部(device / plugin / channel)
// Core ◀─入向审查── 外部
//
// 落地形态是**显式代理**(用户确认的说法只是方便解耦):把 KernelState 上
// 的公共挂载点换成本模块产出的安全对象,业务模块继续认为自己直接对端交互。
//
// ## 依赖与分层位置
//
// config → security → memory / knowledge / agent / device / pluginhost
//
// 只依赖 config:它需要 `core.security.enabled` 与 `ks.DataDir`。
// ★ 刻意**不依赖 agent**:agent 之后要用 `ks.Security`,反之成环。
//
// ## 与 Group 0 的关系(重要)
//
// 密钥与变量库必须在**任何读 core.llm.* 的操作之前**就绪(config 模块的
// 注释记录了 401 invalid api key 的实测故障)。而读 `core.llm.*` 发生在
// `cfgReg.ToConfig()`(Group 0)里,**早于**本调度器。
//
// ⇒ 本模块把密钥与 Store 的装配放进 Init,但**同时**提供
// `Prepare()` 供 Group 0 在 ToConfig 之前调用(幂等)。
// 这正是原先 cmd/homed 里那段"必须前移"的代码的归宿。
package security
import (
"fmt"
"log"
"path/filepath"
"github.com/JianFeeeee/HologramAgent/internal/config"
"github.com/JianFeeeee/HologramAgent/internal/events"
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
sec "github.com/JianFeeeee/HologramAgent/internal/security"
)
// Module 提供安全防火墙设施。
type Module struct{}
func (Module) Name() string { return "security" }
func (Module) Requires() []string { return []string{kernelmod.FacConfig} }
func (Module) Provides() []string { return []string{kernelmod.FacSecurity} }
func (Module) Init(ks *kernelboot.KernelState) error {
if ks.DataDir == "" {
return fmt.Errorf("security module: ks.DataDir is empty - Group 0 must set the data dir first")
}
if ks.CfgReg == nil {
return fmt.Errorf("security module: ks.CfgReg is nil (requires the config facility)")
}
rt, err := Prepare(ks.DataDir, ks.CfgReg)
if err != nil {
return err
}
ks.Security = rt
if ks.EventBus != nil && rt.Enabled() {
ks.EventBus.Publish(secReadyEvent(rt))
}
return nil
}
func init() { kernelmod.Register(Module{}) }
// Prepare 装配安全运行时(**幂等**)。
//
// 可在 Group 0(`cfgReg.ToConfig()` 之前)先调一次,也可以由模块 Init 调。
// 第二次调用直接返回已装好的实例 —— 因为 `InitGlobal` 会重读并重建变量库,
// 而那会在 ToConfig 已经解出密文之后把 Store 换掉,徒增风险。
//
// 返回的 Runtime 里带着 Store 与 Gate(边界闸门)。
func Prepare(dataDir string, cfgReg *config.ConfigRegistry) (*sec.Runtime, error) {
if rt := sec.RuntimeGlobal(); rt != nil {
return rt, nil
}
if cfgReg == nil {
return nil, fmt.Errorf("security: cfgReg is nil")
}
if !cfgReg.GetBool("core.security.enabled", true) {
// 显式关闭:装一个空 Runtime(所有闸门零成本透传),
// 而不是留 nil 让调用点各自判断。
rt := sec.NewDisabledRuntime()
sec.SetRuntimeGlobal(rt)
log.Printf("[security] module disabled by config (core.security.enabled=false)")
return rt, nil
}
// ★ 先收敛敏感文件权限,再读密钥。
//
// 顺序有意为之:内嵌主密钥把密钥放进了 0755 的二进制里,而 config.db
// 出厂是 0644 ——「能读二进制就能解密数据库」。收紧到 0600/0750 是本机
// 缓解手段,必须在服务开始读写之前生效。失败不致命(权限问题不该让服务
// 不可用),但要逐条响亮记录。
if hr := sec.HardenDataDir(dataDir); len(hr.Changed) > 0 {
log.Printf("[security] hardened %d file permissions (necessary mitigation for embedded-key scenario): %v",
len(hr.Changed), hr.Changed)
}
key, keySrc, err := sec.LoadOrCreateKey(dataDir)
if err != nil {
return nil, fmt.Errorf("security key init failed (refusing to start in degraded mode): %w", err)
}
oldKeys, err := sec.LoadKeyring(dataDir, key)
if err != nil {
return nil, fmt.Errorf("security keyring load failed (refusing to silently pick wrong key): %w", err)
}
// ★ 内嵌钥上台时,磁盘钥必须继续参与**解密**。
// 否则用磁盘钥加密过的老机器升级后会全部解不开 ⇒ 服务起不来;
// 用户此时若"重建库",凭据一次性全丢 —— 恰好与内嵌的初衷相反。
if extra := sec.AdditionalDecryptKeys(dataDir, key); len(extra) > 0 {
oldKeys = append(oldKeys, extra...)
}
if len(oldKeys) > 0 {
log.Printf("[security] loaded %d historical/extra key(s) (smooth rotation + embedded migration)", len(oldKeys))
}
store, err := sec.InitGlobal(sec.Options{
Path: filepath.Join(dataDir, "security", "variables.json"),
KeyFn: func() ([]byte, error) { return key, nil },
OldKeys: oldKeys,
Enabled: true,
})
if err != nil {
return nil, fmt.Errorf("security variable store load failed (will not silently rebuild): %w", err)
}
// 配置落盘用的子密钥(域隔离:与别名密钥不共用)。
if _, err := sec.InitConfigCodecWithOldKeys(key, oldKeys); err != nil {
return nil, fmt.Errorf("config-encryption subkey derivation failed: %w", err)
}
// 把库里仍是明文的敏感项就地加密(幂等;升级路径必须跑)。
if n, merr := cfgReg.EncryptExistingSecrets(); merr != nil {
log.Printf("[security] existing sensitive config encryption incomplete (some entries still plaintext): %v", merr)
} else if n > 0 {
log.Printf("[security] encrypted %d existing sensitive config entries", n)
}
// E-2:备份跨机器恢复的诊断(症状极具误导性,一次性说清事实)。
if okN, badN, aerr := cfgReg.AuditSecrets(); aerr == nil && badN > 0 {
log.Printf("[security] WARNING: %d ciphertext entries cannot be decrypted with current master key (%d decryptable) — "+
"likely a config.db restored from another machine without its master key", badN, okN)
}
rt := sec.NewRuntime(store)
sec.SetRuntimeGlobal(rt)
log.Printf("[security] module enabled (key source %s, %d variables, config-at-rest encryption on)",
keySrc, store.Count())
return rt, nil
}
// secReadyEvent 构造"安全防火墙已装配"事件。
//
// 为什么带 enabled 与 variables:状态面要能回答"审查是否真的在生效",
// 而"配置开了但装配失败"与"配置显式关闭"是两件不同的事。
func secReadyEvent(rt *sec.Runtime) *events.Event {
return &events.Event{
Type: events.EventSecurityReady,
Source: "kernelmod/security",
Payload: map[string]interface{}{
"enabled": rt.Enabled(),
"variables": rt.Count(),
},
}
}