feat: 权限档位体系(三档 plan/workspace/full + 四桥 from_session_id)

L2 核心改动:sessions 表补 permission_mode / permission_enforcement 两列
(sqlite + pg 同步),三桥 lib/permission-mode.js 翻译档位到平台原生配置,
homeagent advisory 模式提示词告知模型实际强制力。四桥全部携带 from_session_id
供 relay 去重与会话回溯。

FromHuman / ToHuman 判据已加入心跳 payload 与 notify/mail.go。
This commit is contained in:
2026-09-06 15:16:49 +08:00
parent 13fcb00acc
commit a44fd6949b
32 changed files with 3462 additions and 177 deletions

View File

@ -121,6 +121,21 @@ func Recipients(ctx context.Context, m Mail) {
// 生产实测 pi 与 dsh 互相客套 6 轮直到撞上 hop 上限。
fromHuman, _ := repo.IsHumanUser(ctx, m.From)
// 会话级权限档位与强制力。
//
// 为什么跑在 payload 外:一封邮件可能推给十几个参与方(收件人 + 拄送),
// 而档位是**会话**的属性,每个人都一样 —— 放进闭包里就是每个参与方
// 查一次库。platformFor 那个坑(会话级的值推给所有人)教过的是反面:
// 会话级与参与方级的字段必须分清楚。档位确实是会话级的。
perm, permErr := repo.GetSessionPermission(ctx, m.SessionID)
if permErr != nil {
// 查不到时给默认档 + advisory:不能因为一次查询失败就让插件以为自己拿到了全权。
perm = repo.SessionPermission{
Mode: models.DefaultPermissionMode,
Enforcement: models.EnforcementAdvisory,
}
}
payload := func(role, workspace, forName string) map[string]interface{} {
p := map[string]interface{}{
"mail_id": m.MailID.String(),
@ -160,6 +175,18 @@ func Recipients(ctx context.Context, m Mail) {
// 插件据此不再对 Agent → Agent 的信说「回信不用你自己发」:
// 那句话在那种情形下是假的,而它让模型以为自己只需要「把话说完」。
"from_human": fromHuman,
// permission_mode 声明本任务允许动手到什么程度:plan / workspace / full。
//
// 插件必须把它**翻译成平台原生的沙箱/审批配置**,而不是自己按工具名猜着拦:
// 那会同时违反 I-1(平台原生信号是唯一真相来源)与 I-4(插件只搬运不决策),
// 而且四个插件对「workspace 到底管什么」必然各猜一套。
"permission_mode": perm.Mode,
// permission_enforcement 是建会话时快照的**事实**:native / advisory。
//
// 与 permission_mode 成对下发:前者是要求,后者是对方平台实际做得到。
// 只给前者会让人以为 plan 档把 homeagent 管住了 —— 它的核心没有
// 工具调用拦截点,档位在那里只能写进提示词。
"permission_enforcement": perm.Enforcement,
}
if m.Origin != "" {
p["origin"] = m.Origin