mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
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` 内部先跑全局策略, 所以父的黑名单对子依然有效,子无法提权。
This commit is contained in:
@ -282,6 +282,15 @@ type Agent struct {
|
||||
// builtinHandlers 是交给 device 路由层的族处理器适配器(装配期构造一次)。
|
||||
builtinHandlers agentAPI.BuiltinToolHandlers
|
||||
|
||||
// securityScopeView / securityScopeOnce 是本 agent 的**策略执行视图**
|
||||
// (security 按 agentID 持有,见 security_profile.go 的 securityScope)。
|
||||
//
|
||||
// ★ 为什么是 per-agent 而不是全局:轮次档位(sessionProfile)是该 agent
|
||||
// 独有的状态。放在共享单例上,主 agent 的 guest 会被驻留子的 owner 覆盖
|
||||
// —— 实测可复现(两者同进程并发)。
|
||||
securityScopeView *security.AgentScope
|
||||
securityScopeOnce sync.Once
|
||||
|
||||
// security 是安全防火墙运行时(security 模块产出,可 nil)。
|
||||
//
|
||||
// ★ 可 nil:测试与独立工具(memgc 等)不需要安全模块。
|
||||
@ -599,6 +608,13 @@ func (a *Agent) Start() {
|
||||
func (a *Agent) Stop() {
|
||||
// 父退出**必须**销毁全部驻留子(设计 §10 硬约束:子不得比父活得久、不留孤儿)。
|
||||
a.StopResidents()
|
||||
// ★ 释放本 agent 的策略执行视图(2026-10-31)。
|
||||
//
|
||||
// 视图里存着 **longtimeProfile**(跨轮保留的档位)。不释放的话,
|
||||
// 同名 agent 重建会继承上一代的 long-time 档 —— 那时可能是
|
||||
// "上一个任务留下的访客权限"被新一代默默沿用。
|
||||
// 与主体策略撤销(revokeChildPolicy)同一纪律。
|
||||
a.ReleaseSecurityScope()
|
||||
// 停机前给待办任务补终态。运行中的任务会经 cancel → LLM 失败 → emitResponse
|
||||
// 自然拿到终态,但**从未运行**(排队/待处理)与**已挂起**的任务不会有任何人
|
||||
// 回它们;带 ResponseCh 的同步注入方(cli / clawhubadapter 均无超时)会永久挂起
|
||||
|
||||
@ -98,7 +98,37 @@ func (a *Agent) securityDecisionFor(tool string) security.Decision {
|
||||
if a == nil || a.security == nil {
|
||||
return security.Decision{Allowed: true}
|
||||
}
|
||||
return a.security.CheckToolForAgent(string(a.id), a.parentID != "", tool)
|
||||
// ★ 走本 agent 的**执行视图**(session 档 > longtime 档 > 全局基线)。
|
||||
//
|
||||
// 这是 2026-10-31「每个 agent 一个实例」的落地:视图由 security 按
|
||||
// agentID 持有,本 agent 的 sessionProfile 不会污染别的 agent。
|
||||
return a.securityScope().CheckTool(tool)
|
||||
}
|
||||
|
||||
// securityScope 返回(并缓存)本 agent 的策略执行视图。
|
||||
//
|
||||
// 为什么要缓存:`ForAgent` 是幂等的,但每条输入/每次工具调用都取一次会
|
||||
// 反复加锁;而视图本身在整个 agent 生命周期内不变。
|
||||
func (a *Agent) securityScope() *security.AgentScope {
|
||||
if a == nil || a.security == nil {
|
||||
return nil
|
||||
}
|
||||
a.securityScopeOnce.Do(func() {
|
||||
a.securityScopeView = a.security.ForAgent(string(a.id), a.parentID != "")
|
||||
})
|
||||
return a.securityScopeView
|
||||
}
|
||||
|
||||
// ReleaseSecurityScope 在 agent 销毁时释放它的策略视图。
|
||||
//
|
||||
// ★ 必须调:视图里存着 **longtimeProfile**(跨轮保留的档位)。
|
||||
// agent/子 agent 回收后同名重建是常规操作,残留的 long-time 档会让
|
||||
// 新一代继承上一代父留的约束 —— 与 `RevokePrincipalPolicy` 同一纪律。
|
||||
func (a *Agent) ReleaseSecurityScope() {
|
||||
if a == nil || a.security == nil {
|
||||
return
|
||||
}
|
||||
a.security.ReleaseAgent(string(a.id))
|
||||
}
|
||||
|
||||
// declareChildPolicy 让父为刚创建的子执行体声明主体策略。
|
||||
|
||||
Reference in New Issue
Block a user