|
|
82932821c0
|
feat(security): 每 agent 一个策略实例 —— sessionProfile / longtimeProfile 两层
## 用户裁定
> 每个 agent 一个,分为 sessionProfile 和 longtimeProfile
## 收敛前的缺陷
档位挂在 `Runtime.active`(**进程级单例**)上,多 agent 并发时串档:
主 agent 轮次 A:注入 qq.guest → active = "qq.guest"
驻留子 轮次 B:注入 qq.owner → active = "qq.owner" ← 覆盖 A
主 agent 轮次 A 继续执行工具 → 判定按 owner 走 ← 串档
这不是理论风险:驻留子 / rtchild 与主 agent **同进程并发**(plan.md 十一)。
## 切分原则:共享注册表 + per-agent 执行视图
**不能整份复制 Runtime** —— `profiles` / `principals` / `scopes` 是插件
声明出来的注册表,每个 agent 各持一份就会变成「插件往 A 声明了档位,
B 看不见」,那是最难查的一类缺陷。
共享(进程级 1 份) per-agent(N 份)
──────────────────────── ───────────────────
gate / policy / content sessionProfile ← 轮次级
profiles / principals longtimeProfile ← agent 生命期
scopes / eventPolicy
## 两层档位语义(用户裁定)
sessionProfile —— 属于**这一轮**。输入携带它,End() 即清。
严格 per-round:同一用户下一条消息要重新声明。
longtimeProfile —— 属于**这个 agent 的一段生命**。设置后跨轮保留,
直到显式改掉或 agent 销毁。
**优先级 session > longtime**,这个顺序本身是安全语义:
session 是"这一轮的事实",必须能盖住 long-time 默认值 ——
否则访客发一条消息也无法在长期为 owner 的 agent 上收窄权限。
反向(longtime 盖 session)= 提权:长期 owner 的 agent 上,
访客声明的 guest 被吞掉 ⇒ 访客拿到 owner 权限。
## 改动
① 新增 `internal/security/agentscope.go`:
rt.ForAgent(agentID, isChild) *AgentScope // 幂等
sc.BeginRound(RoundInput) // security 自己解析档位
sc.SetLongtimeProfile(name) / LongtimeProfile()
sc.EffectiveProfile() // session > longtime
sc.CheckTool(tool) // 全局基线 + 档位 + 主体策略
rt.ReleaseAgent(agentID) // 含 longtime 档一起清
② `Runtime` 新增 `scopesByAgent` 表;判定拆出 `checkToolWithProfile(p, profile, tool)`
与 `checkToolForWithProfile(...)` —— **档位由调用方显式传入**,
不再一律读全局 `r.active`(否则 per-agent 视图白做)。
③ `agent/core`:`securityScope()` 用 `sync.Once` 缓存本 agent 的视图;
`Stop()` 里 `ReleaseSecurityScope()`。
④ `Round.End()`:per-agent 路径只清 **session**,**保留** longtime;
旧进程级路径仍恢复进入前的值。
## 判据(3 个变异全部实测咬住)
| 判据 | 变异 |
|---|---|
| `TestAgentScopesDoNotShareSessionProfile` | ForAgent 退化为共享视图 ⇒ 红 |
| `TestLongtimeProfileSurvivesRounds` | 优先级写反 / longtime 不跨轮 ⇒ 红 |
| `TestReleaseAgentDropsLongtimeProfile` | ReleaseAgent 不删视图 ⇒ 红 |
| `TestLongtimeProfileRejectsUndeclared` | 未声明档位被接受 ⇒ 红 |
第一条同时钉:① 各自档位是自己的 ② **行为上**主 agent 仍被 guest 收窄、
子 agent 不受影响(只断言字符串相等会漏掉判定链没接上的情形)。
## 实测
- 真实并发探针:3 个 agent(guest / owner / guest)各 200 轮并发,
`-race` 干净,**零串档**
- build / vet / go test ./...(62 ok / 0 fail)/ -race 全绿
## 未变的约束
档位与主体策略**只能收窄**:`CheckToolFor` 内部先跑全局策略,
所以父的黑名单对子依然有效,子无法提权。
|
2026-10-31 07:24:03 +08:00 |
|
|
|
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 |
|