L2 核心改动:sessions 表补 permission_mode / permission_enforcement 两列 (sqlite + pg 同步),三桥 lib/permission-mode.js 翻译档位到平台原生配置, homeagent advisory 模式提示词告知模型实际强制力。四桥全部携带 from_session_id 供 relay 去重与会话回溯。 FromHuman / ToHuman 判据已加入心跳 payload 与 notify/mail.go。
143 lines
5.8 KiB
Go
143 lines
5.8 KiB
Go
package models
|
||
|
||
// ─── 权限档位 ───
|
||
//
|
||
// 三档描述「这条任务允许 Agent 动手到什么程度」。**AgentMail 声明,平台执行,
|
||
// 插件只做翻译** —— 不能让插件按工具名自己猜着拦,那会同时违反 I-1(平台原生
|
||
// 信号是唯一真相来源)与 I-4(插件只搬运不决策),而且四个插件对「workspace
|
||
// 到底管什么」必然各猜一套。
|
||
//
|
||
// 档位与 DSH 原生的三档沙箱一一对应(read-only / workspace-write /
|
||
// danger-full-access,见 @deepseek-ai/dsh-sandbox-policy)—— 那不是巧合,
|
||
// 是同一个问题的同一个答案。
|
||
const (
|
||
// ModePlan 只读:查资料、读代码、出方案,一个字都不许写。
|
||
//
|
||
// 危险操作**直接拒绝**,不产生权限邮件 —— plan 档的语义就是「这轮不动手」,
|
||
// 没什么可问人的。模型该做的是把方案写在回信里。
|
||
ModePlan = "plan"
|
||
|
||
// ModeWorkspace 本目录内可动手,越界要问人。默认档。
|
||
//
|
||
// 「本目录」= 会话的 workspace(三维地址的 path 位)。越界的定义是
|
||
// 写到那个目录之外,或跑一条无法判定影响范围的命令。
|
||
ModeWorkspace = "workspace"
|
||
|
||
// ModeFull 自动放行,不问人。
|
||
//
|
||
// 不产生权限邮件:既然已经声明了全权,再问一遍只是噪音。
|
||
ModeFull = "full"
|
||
)
|
||
|
||
// DefaultPermissionMode 是没有显式指定时的档位。
|
||
//
|
||
// 选 workspace 而不是 full:默认值应当是「多数任务够用且出错代价可控」的那一档。
|
||
// 一个默认全权的系统里,「我忘了收紧」与「我确实需要全权」在数据上无法区分。
|
||
const DefaultPermissionMode = ModeWorkspace
|
||
|
||
// PermissionModes 是全部合法档位,按宽松程度递增排列。
|
||
//
|
||
// 顺序有意义:ModeAtMost 靠它做「向更严取整」。
|
||
var PermissionModes = []string{ModePlan, ModeWorkspace, ModeFull}
|
||
|
||
// ValidPermissionMode 判断是不是合法档位。
|
||
func ValidPermissionMode(m string) bool {
|
||
for _, v := range PermissionModes {
|
||
if v == m {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// NormalizePermissionMode 把外部输入收敛成合法档位。
|
||
//
|
||
// 空串 → 默认档;非法值 → 默认档(**不是** ModeFull)。
|
||
// 拼错一个档位名不该换来比预期更大的权限。
|
||
func NormalizePermissionMode(m string) string {
|
||
if ValidPermissionMode(m) {
|
||
return m
|
||
}
|
||
return DefaultPermissionMode
|
||
}
|
||
|
||
// modeRank 是档位的宽松程度序号,越大越宽松。
|
||
//
|
||
// 只接已经归一化过的档位 —— 调用方负责先跑 NormalizePermissionMode。
|
||
// 让它自己处理非法值会造出两套语义:曾经这里把未知值当 rank 0(plan),
|
||
// 而 NormalizePermissionMode 把它归到 workspace,于是同一个脏值在不同函数里
|
||
// 含义不同,ModeAtMost 也因此不可交换(单元测试当场抓到)。
|
||
func modeRank(m string) int {
|
||
for i, v := range PermissionModes {
|
||
if v == m {
|
||
return i
|
||
}
|
||
}
|
||
// 归一化后不可能走到这里;防御性地返回默认档的序号。
|
||
return modeRank(DefaultPermissionMode)
|
||
}
|
||
|
||
// ModeAtMost 返回 a 与 b 里更严的那一档。
|
||
//
|
||
// 两个用途:
|
||
// - 子会话继承:Agent 派活时子会话不得比父会话宽松(plan 档派不出 full 档子任务)
|
||
// - 平台取整:平台表达不出精确档位时向更严的方向取整
|
||
//
|
||
// 为什么必须是同一个函数:这两处若各写一遍,早晚有一处会写成「取更宽松」。
|
||
//
|
||
// **先归一化再比较**:两个脏值都变成默认档,于是结果与参数顺序无关(可交换),
|
||
// 也与 NormalizePermissionMode / ModeNeedsHuman 对同一个脏值的理解一致。
|
||
func ModeAtMost(a, b string) string {
|
||
na := NormalizePermissionMode(a)
|
||
nb := NormalizePermissionMode(b)
|
||
if modeRank(na) <= modeRank(nb) {
|
||
return na
|
||
}
|
||
return nb
|
||
}
|
||
|
||
// ModeNeedsHuman 这一档会不会产生权限邮件(即需不需要人来点头)。
|
||
//
|
||
// 只有 workspace 档需要人。这一点直接决定了「找不到人类时怎么办」:
|
||
// plan 档当场拒绝、full 档自动放行,两者都不问人,所以**只有 workspace 档
|
||
// 会走到「这条链上有没有人类」这个问题**,找不到就是 409。
|
||
//
|
||
// 这也是为什么 permission.go 里那段「退回第一个管理员」的兜底必须删掉:
|
||
// 它让 409 分支永远不可达(实测:pi 给自己派活跑 bash,权限邮件发给了 jianf),
|
||
// 而那段 409 的注释本身就在论证兜底是错的 —— 管理员对这条 Agent 链一无所知。
|
||
func ModeNeedsHuman(m string) bool {
|
||
return NormalizePermissionMode(m) == ModeWorkspace
|
||
}
|
||
|
||
// ─── 强制力 ───
|
||
//
|
||
// 档位是「要求什么」,强制力是「平台实际做到了什么」。两者必须分开记录并且
|
||
// 都对人可见(I-5:失败必须可见)—— 否则发件人以为 plan 档管住了 homeagent,
|
||
// 而 homeagent 的核心根本没有工具调用拦截点。
|
||
const (
|
||
// EnforcementNative 平台有原生拦截点,档位被真正执行。
|
||
EnforcementNative = "native"
|
||
|
||
// EnforcementAdvisory 平台没有拦截点,档位只写进提示词。
|
||
//
|
||
// 模型至少知道「这活只让你看不让你动」,但没有任何机制阻止它动手。
|
||
// 这不是缺陷掩饰 —— 是把「做不到」如实标出来,让发件人自己决定要不要派。
|
||
EnforcementAdvisory = "advisory"
|
||
)
|
||
|
||
// ValidEnforcement 判断强制力取值是否合法。
|
||
func ValidEnforcement(e string) bool {
|
||
return e == EnforcementNative || e == EnforcementAdvisory
|
||
}
|
||
|
||
// NormalizeEnforcement 收敛强制力取值。
|
||
//
|
||
// 空串或非法值 → advisory。**保守方向是 advisory 而不是 native**:
|
||
// 没自报过的插件,我们不能替它宣称「档位在这里是被强制的」。
|
||
func NormalizeEnforcement(e string) string {
|
||
if ValidEnforcement(e) {
|
||
return e
|
||
}
|
||
return EnforcementAdvisory
|
||
}
|