mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
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:
@ -59,16 +59,15 @@ func (Module) Init(k *kernelboot.KernelState) error {
|
||||
|
||||
func init() { kernelmod.Register(Module{}) }
|
||||
|
||||
const defaultSystemPrompt = `你是 HologramAgent,一个持续运行的个人管家。
|
||||
const defaultSystemPrompt = `You are HologramAgent, a continuously running personal assistant.
|
||||
|
||||
可用工具与已安装技能由系统在每轮自动注入:
|
||||
- 工具的 description 写了「何时该用、参数含义、有什么坑」,调用前先读它,不要凭工具名猜用法;
|
||||
- 技能索引用 skill_info 取全文,多步流程按技能文档执行。
|
||||
The system injects the available tools and installed skills on every turn:
|
||||
- Each tool's description states when to use it, what its parameters mean, and known pitfalls. Read it before calling; never guess usage from the tool name.
|
||||
- The skill index is only an index. Use skill_info to read a skill in full, and follow its documented steps for multi-step procedures.
|
||||
|
||||
只根据工具真实返回的内容作答:没取到就如实说没取到,不要补全、猜测或编造。
|
||||
Answer only from what the tools actually return. If something could not be retrieved, say so plainly. Never fill gaps by completing, guessing, or inventing.
|
||||
|
||||
回复用自然语言,像跟人聊天:不要罗列调用了哪个工具、参数是什么、耗时多少,
|
||||
除非用户明确要求。称呼、语气与说话方式见【人格设定】。`
|
||||
Reply in natural language, like a person talking: do not enumerate which tools you called, their parameters, or timings unless the user explicitly asks. For how to address the user and what tone to use, see [Persona].`
|
||||
|
||||
func buildAgent(k *kernelboot.KernelState) *agentCore.Agent {
|
||||
cfg := k.Cfg
|
||||
|
||||
@ -26,6 +26,7 @@ import (
|
||||
// (见 kernelmod/core.go)。真正的先后只由 Requires/Provides 决定。
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/agent"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/config"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/device"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/docstore"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/kernelapi"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/knowledge"
|
||||
@ -36,6 +37,7 @@ import (
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/multimodal"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/onnx"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/pluginhost"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/security"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/stagehost"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/supervisor"
|
||||
_ "github.com/JianFeeeee/HologramAgent/internal/kernelmod/textmem"
|
||||
|
||||
26
internal/kernelmod/device/dispatch.go
Normal file
26
internal/kernelmod/device/dispatch.go
Normal file
@ -0,0 +1,26 @@
|
||||
package device
|
||||
|
||||
// device 模块对内置工具路由的**对外入口**。
|
||||
//
|
||||
// ## 为什么路由表本体在 internal/toolroute
|
||||
//
|
||||
// 依赖方向限制:agent/core 需要调用路由,而 kernelmod/device 会 import
|
||||
// kernelboot(→ agent/core)。若路由表放在本包,core 就会 import 本包,
|
||||
// 形成 core → device → kernelboot → core 的**循环**。
|
||||
//
|
||||
// 所以:路由表放低层包(toolroute,只依赖 agent/api),
|
||||
// 本包保留对外入口 —— 模块的所有权仍然在这里(它是 device 模块的
|
||||
// 第一个被搬出来的部件),只是实现下沉以避免环。
|
||||
|
||||
import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/toolroute"
|
||||
)
|
||||
|
||||
// Dispatch 按工具名选出族处理器并执行(device 模块的对外入口)。
|
||||
func Dispatch(h api.BuiltinToolHandlers, tc api.ToolCall, turnScenes []string) (string, bool) {
|
||||
return toolroute.Dispatch(h, tc, turnScenes)
|
||||
}
|
||||
|
||||
// IsBuiltinTool 报告某名字是否由内置路由处理。
|
||||
func IsBuiltinTool(name string) bool { return toolroute.IsBuiltinTool(name) }
|
||||
143
internal/kernelmod/device/mod.go
Normal file
143
internal/kernelmod/device/mod.go
Normal file
@ -0,0 +1,143 @@
|
||||
// Package device 是 device 模块:**工具数据面的统一入口**。
|
||||
//
|
||||
// ## 定位(用户裁定 2026-10-26)
|
||||
//
|
||||
// > toolcall 部分作为一个完整的 device 子模块,管理全部的 tool、inputch 和
|
||||
// > outputch。
|
||||
//
|
||||
// 第一阶段(本文件)先建立**正式模块 + 数据面对象 + KernelState 挂载点**,
|
||||
// 工具执行仍委托 agent —— 行为零变化,但边界成立、安全检查有唯一落点。
|
||||
// 第二阶段再把 tooldefs/toolcall 的实现迁进来(见 plan.md §七)。
|
||||
//
|
||||
// 为什么不一锅端:搬 2000+ 行工具实现的同时还有 1000+ 判据在跑,
|
||||
// 任何一处不一致都无从定位。先把**边界与闸门**立起来,再搬实现。
|
||||
//
|
||||
// ## 分层位置
|
||||
//
|
||||
// config → security → memory/knowledge → stagehost → agent → device → pluginhost
|
||||
//
|
||||
// ## 安全纪律
|
||||
//
|
||||
// device 不实现安全逻辑,也不持有 raw security.Store:
|
||||
// 只通过 ks.Security 的 Runtime 过境。任何"顺手直接调 store"的写法都是
|
||||
// 绕过闸门(判据见 internal/security/gate_test.go)。
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/JianFeeeee/HologramAgent/internal/agent/api"
|
||||
"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 "device" }
|
||||
|
||||
func (Module) Requires() []string {
|
||||
return []string{kernelmod.FacAgent, kernelmod.FacSecurity}
|
||||
}
|
||||
|
||||
func (Module) Provides() []string { return []string{kernelmod.FacDevice} }
|
||||
|
||||
func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
if ks.Agent == nil {
|
||||
return fmt.Errorf("device module: ks.Agent is nil (requires %s)", kernelmod.FacAgent)
|
||||
}
|
||||
if ks.Security == nil {
|
||||
// 安全模块必须已装配(配置关闭时是 Disabled Runtime,**不是** nil)。
|
||||
// nil 表示装配顺序错了 —— 那会让本模块"看起来装好了但没有闸门"。
|
||||
return fmt.Errorf("device module: ks.Security is nil - the security module must be " +
|
||||
"assembled first (when disabled it installs a Disabled Runtime, not nil)")
|
||||
}
|
||||
|
||||
rt := New(ks.Agent, ks.Security)
|
||||
ks.Device = rt
|
||||
ks.Agent.MountDevice(rt)
|
||||
|
||||
if ks.EventBus != nil {
|
||||
ks.EventBus.Publish(&events.Event{
|
||||
Type: events.EventDeviceReady,
|
||||
Source: "kernelmod/device",
|
||||
Payload: map[string]interface{}{
|
||||
"tools": rt.ToolCount(),
|
||||
"secured": rt.Secured(),
|
||||
},
|
||||
})
|
||||
}
|
||||
log.Printf("[device] ready: %d tools, security=%v", rt.ToolCount(), rt.Secured())
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() { kernelmod.Register(Module{}) }
|
||||
|
||||
// Runtime 是 device 数据面。
|
||||
type Runtime struct {
|
||||
host api.ToolHost
|
||||
sec *sec.Runtime
|
||||
}
|
||||
|
||||
// New 构造 device 数据面。
|
||||
func New(host api.ToolHost, s *sec.Runtime) *Runtime {
|
||||
return &Runtime{host: host, sec: s}
|
||||
}
|
||||
|
||||
// Security 返回安全运行时(只读,供过境用;不是变量库本身)。
|
||||
func (r *Runtime) Security() *sec.Runtime {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.sec
|
||||
}
|
||||
|
||||
// Secured 报告安全审查是否真的生效(状态面用)。
|
||||
func (r *Runtime) Secured() bool { return r != nil && r.sec != nil && r.sec.Enabled() }
|
||||
|
||||
// Tools 返回全部工具定义(按名字排序,便于人读与稳定输出)。
|
||||
func (r *Runtime) Tools() []api.ToolDefinition {
|
||||
if r == nil || r.host == nil {
|
||||
return nil
|
||||
}
|
||||
out := r.host.ToolDefinitions()
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
// ToolCount 返回工具数。
|
||||
func (r *Runtime) ToolCount() int { return len(r.Tools()) }
|
||||
|
||||
// Execute 执行一次工具调用,并对**返回值**做出向边界处理。
|
||||
//
|
||||
// ★ 这是 device 的执行入口,但**不是**安全审查的唯一位置:
|
||||
// - 参数校验 / 工具授权 / 设备授权 / 超时 / panic 恢复 → ToolHost(agent/core)
|
||||
// - 明文 → 别名(出向)、别名 → 明文(执行前回填) → agent 的 stage 管道
|
||||
// - 返回值清洗(入向)→ 本函数与 agent 的 defer 双保险
|
||||
//
|
||||
// 为什么入向要在两处都做:agent 的 defer 覆盖全部 return 出口(含 panic),
|
||||
// 而本函数覆盖"经 device 的调用"。少一处就可能有一条路径不洗。
|
||||
func (r *Runtime) Execute(name string, args map[string]interface{}) (string, error) {
|
||||
if r == nil || r.host == nil {
|
||||
return "", fmt.Errorf("device: tool host is not mounted")
|
||||
}
|
||||
out, err := r.host.ExecuteTool(name, args)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if r.sec != nil {
|
||||
cleaned, n := r.sec.ScrubInbound(sec.Inspection{
|
||||
Direction: sec.DirectionInbound,
|
||||
Stage: "tool_result",
|
||||
Tool: name,
|
||||
}, out)
|
||||
if n > 0 {
|
||||
return cleaned, nil
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@ -53,4 +53,17 @@ const (
|
||||
// 为什么在内部标记段:它是内核自用的启动时序事实,不是插件可选的
|
||||
// 外部契约(插件声明它只会立刻满足,没有意义)。
|
||||
FacVersion = "kernel.version"
|
||||
|
||||
// FacSecurity 是**安全防火墙**设施(security 模块提供)。
|
||||
//
|
||||
// ★ 它不是"某个能力",而是横跨 Core 与全部外部模块的**数据面闸门**:
|
||||
// Core → 外部、外部 → Core 的每一条数据流都要经过它(显式代理)。
|
||||
// 需要被安全审查才能访问外部模块的模块声明依赖它。
|
||||
FacSecurity = "security"
|
||||
|
||||
// FacDevice 是**设备/工具数据面**设施(device 模块提供)。
|
||||
//
|
||||
// 统一拥有:工具注册与分发、inputch、outputch、设备登记。
|
||||
// 它不是"新的一层存储",而是模型可见能力的唯一入口。
|
||||
FacDevice = "device"
|
||||
)
|
||||
|
||||
173
internal/kernelmod/security/mod.go
Normal file
173
internal/kernelmod/security/mod.go
Normal 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
41
internal/kernelmod/security/profile_bridge.go
Normal file
41
internal/kernelmod/security/profile_bridge.go
Normal file
@ -0,0 +1,41 @@
|
||||
package security
|
||||
|
||||
// 把 **SDK 的声明面** 桥接到内核的 security.Runtime。
|
||||
//
|
||||
// 分工(用户 2026-10-26 的模型):
|
||||
//
|
||||
// 插件(子进程/动态库)
|
||||
// │ DeclareProfile(...) / InjectOptions.SecurityProfile = "qq.guest"
|
||||
// ▼
|
||||
// proc 桥 / SDK 转发
|
||||
// │
|
||||
// ▼
|
||||
// security.Runtime.DeclareProfile / ActivateProfile ← 内核持有与执行
|
||||
//
|
||||
// 为什么必须由内核持有:插件崩溃或被重载时,声明不能跟着消失
|
||||
//(否则挂掉插件即可绕过策略);且判定要与全局/主体策略走同一条链。
|
||||
|
||||
import (
|
||||
sec "github.com/JianFeeeee/HologramAgent/internal/security"
|
||||
)
|
||||
|
||||
// DeclareProfile 把一个插件声明的命名限式登记进内核运行时。
|
||||
//
|
||||
// 语义与 sec.Profile 一致:名字必须是 `<owner>.<档位>`,同一 owner 可覆盖自己。
|
||||
func DeclareProfile(rt *sec.Runtime, p sec.Profile) error {
|
||||
if rt == nil {
|
||||
return sec.ErrNoRuntimeForBridge
|
||||
}
|
||||
return rt.DeclareProfile(p)
|
||||
}
|
||||
|
||||
// ActivateProfile 在 inputch 输入进来时激活插件选择的档位。
|
||||
//
|
||||
// 调用点约定:注入层拿到 `InjectOptions.SecurityProfile` 后调用一次。
|
||||
// 空串 = 显式不使用档位(沿用内核默认策略),这是**可选参数**的缺省语义。
|
||||
func ActivateProfile(rt *sec.Runtime, name string, ttlSeconds int) error {
|
||||
if rt == nil {
|
||||
return sec.ErrNoRuntimeForBridge
|
||||
}
|
||||
return rt.ActivateProfile(name, sec.TTLSeconds(ttlSeconds))
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
@ -27,7 +28,16 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
if err := trk.Init(); err != nil {
|
||||
log.Printf("[homed] warning: tracker init: %v", err)
|
||||
} else if err := trk.Start(); err != nil {
|
||||
log.Printf("[homed] warning: tracker mount overlay: %v (non-fatal: no overlayfs support?)", err)
|
||||
if errors.Is(err, tracker.ErrOverlayNoPrivilege) {
|
||||
// 用户 2026-10-26 拍板方案 A:服务降权运行,接受变更追踪降级。
|
||||
// 日志必须说清"是什么、影响什么、怎么恢复",而不是"可能不支持"。
|
||||
log.Printf("[homed] change tracker degraded: agentfs overlay needs mount privilege, " +
|
||||
"service runs as non-root (decision A, 2026-10-26). " +
|
||||
"Consequence: agentfs file-change tracking/rollback is disabled; chat, memory, " +
|
||||
"plugins and tools are unaffected. To restore: run as root, or grant mount capability.")
|
||||
} else {
|
||||
log.Printf("[homed] warning: tracker mount overlay: %v (non-fatal)", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[homed] change tracker active at %s", trk.MergeDir())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user