Files
HomeAgent/internal/kernelmod/security/policy_config.go
JianFeeeee b850feaef2 feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边

命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据

安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据

验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
2026-10-29 23:53:48 +08:00

152 lines
5.6 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
// 把**部署方配置**(`core.security.*`)装配成内核全局策略基线。
//
// ## 为什么全局基线需要有配置入口(用户 2026-10-29 裁定:选项 B)
//
// 策略有两层,职责不同:
//
// ① 全局基线 security.Policy ← 本文件负责:部署方兜底("这台机器不许跑 cmd_*")
// ② 档位 Profile / 作用域 Scope ← 插件声明:单次请求的收窄("这个 QQ 访客不行")
//
// ②是**声明式**且**只能收窄**(见 Runtime.CheckTool:全局黑名单先判,
// 档位绕不过去)。但②有它的结构性盲区:**部署方无法阻止一个恶意/写坏的
// 插件**——插件自己声明档位,等于被约束者决定约束。所以必须有一层
// 不受插件影响的基线,且它要能在不重编译的前提下改 —— 那就是①。
//
// ## 为什么不放在 internal/security 读配置
//
// `internal/security` 不 import `internal/config`(否则配置包与安全包
// 互相依赖,且 security 会被迫感知 SQLite)。装配放在本桥接层,
// security 保持纯策略/算法包。
//
// ## 为什么是启动期一次快照(不热重载)
//
// `Policy` 在**每次工具调用**的热路径上被查;查 SQLite 不可接受。
// 策略变更本就低频,且"安全策略在运行中被悄悄改变"本身是可疑行为。
// 因此:启动期读一次,改配置需重启(与 core.agent.offload_enabled 同纪律)。
import (
"fmt"
"log"
"strings"
sec "github.com/JianFeeeee/HologramAgent/internal/security"
)
// ConfigReader 是本桥接层需要的最小配置面(便于测试注入假实现)。
type ConfigReader interface {
GetString(key, defaultVal string) string
GetBool(key string, defaultVal bool) bool
}
// PolicyFromConfig 从配置读出一份全局策略快照。
//
// 缺省语义(重要):所有项留空/false 时返回**零值 Policy**,
// 即"什么都不限制",与加配置之前的存量行为逐字一致。
// 这条是升级安全的底线:老安装升级后行为不得改变。
func PolicyFromConfig(cfg ConfigReader) sec.Policy {
if cfg == nil {
return sec.DefaultPolicy()
}
p := sec.Policy{
ToolDeny: splitToolList(cfg.GetString("core.security.tool_deny", "")),
ToolAllow: splitToolList(cfg.GetString("core.security.tool_allow", "")),
Sandbox: sec.SandboxPolicy{
Enabled: cfg.GetBool("core.security.sandbox.enabled", false),
ReadRoots: splitPathList(cfg.GetString("core.security.sandbox.read_roots", "")),
WriteRoots: splitPathList(cfg.GetString("core.security.sandbox.write_roots", "")),
AllowNetwork: cfg.GetBool("core.security.sandbox.allow_network", true),
},
}
// 自相矛盾的配置(同名工具同时在黑白名单)不静默择一:
// CheckTool 的语义是 deny 优先,但那种配置通常意味着写错了,
// 而"写错的安全策略悄悄偏向一边"比启动失败更危险。这里只响亮记录。
if both := intersect(p.ToolDeny, p.ToolAllow); len(both) > 0 {
log.Printf("[security] WARNING: tool(s) %v appear in BOTH tool_deny and tool_allow; "+
"deny wins (denylist is evaluated first), but this configuration is likely a mistake", both)
}
return p
}
// ApplyPolicyFromConfig 读取配置并把策略装进运行时。
//
// 返回装好的策略,便于调用方记录/断言。rt 为 nil 时安全跳过
// (未装配安全模块的独立工具/测试)。
func ApplyPolicyFromConfig(rt *sec.Runtime, cfg ConfigReader) sec.Policy {
p := PolicyFromConfig(cfg)
if rt != nil {
rt.SetPolicy(p)
}
if !p.Sandbox.Enabled && len(p.ToolDeny) == 0 && len(p.ToolAllow) == 0 {
// 零值策略:不打印噪音日志(存量安装的常态)。
return p
}
log.Printf("[security] global policy applied: deny=%v allow=%v sandbox=%v (read=%d roots, write=%d roots, network=%v)",
p.ToolDeny, p.ToolAllow, p.Sandbox.Enabled,
len(p.Sandbox.ReadRoots), len(p.Sandbox.WriteRoots), p.Sandbox.AllowNetwork)
return p
}
// splitToolList 解析逗号分隔的工具名单。
//
// 空串 ⇒ nil(而不是 [""])—— 后者在 CheckTool 里会被当成"有一个空模式",
// 虽然 matchTool 对空模式返回 false 不会误伤,但 nil 语义更干净:
// "没配置"与"配置了一个空项"应当可区分。
func splitToolList(s string) []string {
return splitNonEmpty(s)
}
// splitPathList 解析逗号分隔的路径前缀。
//
// 去尾部分隔符:`/tmp/` 与 `/tmp` 应等价,否则 `filepath.HasPrefix` 式的
// 前缀匹配会把 `/tmpfoo` 误判进 `/tmp`。规范化交给 CheckPath(它会 Clean)。
func splitPathList(s string) []string {
out := splitNonEmpty(s)
for i, v := range out {
out[i] = strings.TrimRight(v, "/")
if out[i] == "" {
// "/" 被 TrimRight 成空 —— 根目录是个合法的意图,还原回去。
out[i] = "/"
}
}
return out
}
func splitNonEmpty(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
var out []string
for _, part := range strings.Split(s, ",") {
if v := strings.TrimSpace(part); v != "" {
out = append(out, v)
}
}
return out
}
// intersect 返回两个名单的交集(用于诊断自相矛盾的配置)。
func intersect(a, b []string) []string {
if len(a) == 0 || len(b) == 0 {
return nil
}
set := make(map[string]bool, len(b))
for _, v := range b {
set[v] = true
}
var out []string
for _, v := range a {
if set[v] {
out = append(out, v)
}
}
return out
}
// String 便于日志里一眼看出策略是否为空(诊断用)。
func DescribePolicy(p sec.Policy) string {
return fmt.Sprintf("deny=%d allow=%d sandbox=%v", len(p.ToolDeny), len(p.ToolAllow), p.Sandbox.Enabled)
}