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:
@ -14,8 +14,8 @@ import (
|
||||
// ---------- /me:当前登录人类用户的邮箱(全部路由需 UserAuth) ----------
|
||||
|
||||
type meSendMailRequest struct {
|
||||
To string `json:"to"` // name@path.session
|
||||
CC string `json:"cc"` // 多个 name@path.session
|
||||
To string `json:"to"` // name@path.session
|
||||
CC string `json:"cc"` // 多个 name@path.session
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
ReplyTo string `json:"reply_to"`
|
||||
@ -30,6 +30,16 @@ type meSendMailRequest struct {
|
||||
// 仅在本次投递【新建】会话时生效;续谈已有会话请用
|
||||
// PUT /sessions/{id}/budget(对话页里可随时改)。
|
||||
MaxRounds *int `json:"max_rounds"`
|
||||
|
||||
// PermissionMode 声明本任务允许 Agent 动手到什么程度:plan / workspace / full。
|
||||
//
|
||||
// 与 MaxRounds 同理,**仅在本次投递【新建】会话时生效**:续谈已有会话若也接受
|
||||
// 这个字段,每封新信都会悄悄改掉对方正在遵守的规则 —— 而 plan 档的会话里
|
||||
// 模型已经被告知「只许看」,第二封信把它改成 full 是在一段已有上下文里换规则。
|
||||
// 续谈请用 PUT /sessions/{id}/permission(对话页里可随时改)。
|
||||
//
|
||||
// 省略时用 models.DefaultPermissionMode(workspace)。
|
||||
PermissionMode string `json:"permission_mode"`
|
||||
}
|
||||
|
||||
// POST /api/v1/me/mail/send
|
||||
@ -65,6 +75,11 @@ func MeSendMail(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 附件可挂性必须在**建邮件之前**校验(与 Agent 侧同理,见 mail.go)。
|
||||
if !checkAttachable(w, r, attachIDs, user.Username) {
|
||||
return
|
||||
}
|
||||
|
||||
// human@ 是兼容别名,人类发信时解析为自己
|
||||
to = resolveHumanAlias(to, user.Username)
|
||||
for i := range ccList {
|
||||
@ -82,7 +97,24 @@ func MeSendMail(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, parentMailID, err := resolveTarget(r, to, req.ReplyTo, user.Username, req.Subject, req.SessionAlias, "")
|
||||
// 纯输入校验必须在建会话【之前】做完。
|
||||
//
|
||||
// 原来两项校验都在 resolveTarget 之后:请求返回 400,但 `.new` 已经建好了
|
||||
// 会话、占掉了新建速率名额、并留下一条谁也不会再用的空线索。实测发 5 封
|
||||
// 非法请求就攒下 5 条垃圾会话。校验不依赖会话,本来就该先做。
|
||||
rounds := -1
|
||||
if req.MaxRounds != nil {
|
||||
if *req.MaxRounds < 0 {
|
||||
Error(w, http.StatusBadRequest, "max_rounds 不能为负")
|
||||
return
|
||||
}
|
||||
rounds = *req.MaxRounds
|
||||
}
|
||||
if !validPermissionModeInput(w, req.PermissionMode) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, parentMailID, created, err := resolveTarget(r, to, req.ReplyTo, user.Username, req.Subject, req.SessionAlias, "")
|
||||
if err != nil {
|
||||
writeErr(w, err, "Failed to resolve session")
|
||||
return
|
||||
@ -95,21 +127,30 @@ func MeSendMail(w http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// 没显式给就用【收件 Agent 的默认值】。默认值挂在 Agent 上而不是全站一个数:
|
||||
// 跑测试的小工具与重构整个模块的 Agent,合理来回数差一个量级。
|
||||
if parentMailID == nil {
|
||||
rounds := 0
|
||||
if req.MaxRounds != nil {
|
||||
if *req.MaxRounds < 0 {
|
||||
Error(w, http.StatusBadRequest, "max_rounds 不能为负")
|
||||
return
|
||||
}
|
||||
rounds = *req.MaxRounds
|
||||
} else {
|
||||
// 判据是 `created` 而不是 `parentMailID == nil`:后者在「省略 session 位复用
|
||||
// 默认会话」时也成立,于是第二封信会把对方正在遵守的预算改写成默认值
|
||||
//(实测:max_rounds=7 的会话被第二封省略该字段的信改成 20)。
|
||||
if created {
|
||||
if rounds < 0 {
|
||||
rounds = repo.DefaultRoundsFor(r.Context(), to.Name)
|
||||
}
|
||||
if _, err := repo.SetSessionBudget(r.Context(), sessionID, rounds); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "Failed to set session budget")
|
||||
return
|
||||
}
|
||||
|
||||
// 权限档位同样只在新建时定。人可以直接指定(不继承)—— 人就是权限的源头,
|
||||
// 而 Agent 侧的 SendMail 走 InheritedMode 不得自行抬档。
|
||||
mode := models.NormalizePermissionMode(req.PermissionMode)
|
||||
if _, err := repo.SetSessionPermissionMode(r.Context(), sessionID, mode); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "Failed to set permission mode")
|
||||
return
|
||||
}
|
||||
// 强制力是事实快照:按收件 Agent 当下自报的能力定死。
|
||||
// 收件方是人类时也走这里 —— AgentModeEnforcement 查不到就返回 advisory,
|
||||
// 而人的收件箱本来不执行任何档位,这个值对他无意义也无害。
|
||||
_ = repo.SetSessionEnforcement(r.Context(), sessionID,
|
||||
repo.AgentModeEnforcement(r.Context(), to.Name))
|
||||
}
|
||||
|
||||
// 人类侧不产生改名提议(人直接有改名按钮,用不着向自己提议),
|
||||
@ -124,6 +165,9 @@ func MeSendMail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if !attachAll(w, r, mailID, attachIDs, user.Username) {
|
||||
// 竞态窗口(见 mail.go 同位置):回滚那封已入库的邮件。
|
||||
// 人类发信不扣会话预算、也不走 relay,所以只需退邮件本身。
|
||||
_ = repo.DeleteMailByID(r.Context(), mailID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user