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 }