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:
@ -47,15 +47,26 @@ type heartbeatRequest struct {
|
||||
// 空数组 = 平台确实一个模型都拿不到。拿不到目录时必须省略:
|
||||
// 清空目录会让配置页变成空白,管理员以为该平台没有任何可用模型。
|
||||
Models []repo.CatalogModel `json:"models"`
|
||||
|
||||
// ModeEnforcement 是插件自报的权限档位强制能力:native / advisory。
|
||||
//
|
||||
// 为什么走心跳而不是注册:能力会在运行中变。DSH 的沙箱模式被改成
|
||||
// danger-full-access 时,它就从 native 退化成了 advisory(实测:
|
||||
// approval:"never" 会在 waterfall 之前短路,approval/request 根本不触发)。
|
||||
// 只在注册时报一次的话,发件人看到的是上次重启时的能力快照。
|
||||
//
|
||||
// 与模型目录同一条通道(I-1:平台自己说的才算)。
|
||||
// 省略 = 本次不上报,保留现有值(与 PlatformSessions / Models 同约定)。
|
||||
ModeEnforcement string `json:"mode_enforcement"`
|
||||
}
|
||||
|
||||
// POST /api/v1/agent/register
|
||||
//
|
||||
// 两种认证方式:
|
||||
// 1. Authorization: Bearer <agent_key_token> —— 密钥认证(推荐)。
|
||||
// 密钥未绑定时用本请求的 name 落定;已绑定时 name 必须与之一致,
|
||||
// 否则等于拿别人的密钥冒充新身份。
|
||||
// 2. body 里带 secret —— 旧方式,兼容保留。
|
||||
// 1. Authorization: Bearer <agent_key_token> —— 密钥认证(推荐)。
|
||||
// 密钥未绑定时用本请求的 name 落定;已绑定时 name 必须与之一致,
|
||||
// 否则等于拿别人的密钥冒充新身份。
|
||||
// 2. body 里带 secret —— 旧方式,兼容保留。
|
||||
func RegisterAgent(w http.ResponseWriter, r *http.Request) {
|
||||
var req registerRequest
|
||||
if !DecodeBody(w, r, &req) {
|
||||
@ -161,9 +172,14 @@ func HeartbeatAgent(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 可选的平台会话快照。解不开就当作没带:心跳的主职责是「我还活着」,
|
||||
// 不该因为上报体格式不对就把 Agent 判成离线。
|
||||
//
|
||||
// 但**未知字段必须回报**(resp["unknown_fields"]):这是全站唯一一处宽容
|
||||
// 解码的端点,若还静默忽略,插件把 `models` 拼成 `modles` 就永远没人知道 ——
|
||||
// 而那与 `attachments` vs `attachment_ids` 是同一种事故形状。
|
||||
var req heartbeatRequest
|
||||
var unknownFields []string
|
||||
if r.ContentLength > 0 {
|
||||
_ = Decode(r, &req)
|
||||
unknownFields, _ = DecodeLenient(r, &req)
|
||||
}
|
||||
syncedSessions := -1 // -1 = 本次未上报
|
||||
if req.PlatformSessions != nil {
|
||||
@ -183,6 +199,12 @@ func HeartbeatAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 档位强制能力:省略时不动(保留现有值)。
|
||||
// 写失败不影响心跳本身 —— 心跳的主职责是「我还活着」。
|
||||
if req.ModeEnforcement != "" {
|
||||
_ = repo.SetAgentModeEnforcement(r.Context(), agentName, req.ModeEnforcement)
|
||||
}
|
||||
|
||||
// 心跳回传该 Agent 的累计统计与新任务默认预算。
|
||||
//
|
||||
// 不再回传「剩余额度」:额度属于具体任务(会话)而不属于 Agent,
|
||||
@ -212,6 +234,11 @@ func HeartbeatAgent(w http.ResponseWriter, r *http.Request) {
|
||||
resp["allowed_models"] = allowed
|
||||
resp["models_unrestricted"] = len(allowed) == 0
|
||||
}
|
||||
// 未知字段回报:只有真的出现时才带这一项,正常心跳的响应不多一个空数组。
|
||||
// 插件看到它就知道自己上报的某个字段服务端根本没收。
|
||||
if len(unknownFields) > 0 {
|
||||
resp["unknown_fields"] = unknownFields
|
||||
}
|
||||
JSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user