feat(gateway,admin-web): Skill 运行时安装 + backthink 打扰原因 + 建提醒入口(P0-19/P0-20/P0-22,审计 §4.3)
- P0-19:POST /admin/plugins/install 支持 `kind:"skill"`(此前一律 400)——yaml 或 path 二选一、 name 一致性校验、显式错误码(bad_request/name_mismatch/skill_parse_failed/name_conflict)、 装后以 kind:"skill" 出现在 GET /admin/plugins;**文件来源解析错误绝不回显文件内容**(变异验证实测确认 该泄露向量真实存在:yaml 解码器会带出出错值前几字符 ⇒ 只回显路径 + 通用原因,细节进日志) - P0-20:AdminBackthinkItem 补 route/detail/bypassed(DB 早已存,零迁移),面板 Backthink 展示 - P0-22:新增 POST /admin/automations/reminders(owner-only + 审计),at/message 必填、RFC3339 校验、 409 already_exists;过去时间不作 400(下次轮询立即触发) - 顺手修真缺陷:新建 id 原用 time.Now().UnixNano(),Windows 时钟粒度粗到毫秒 ⇒ 并行创建撞 id 得假 409; 改用 crypto/rand 128 位 hex(仓库既有 skills/store.go 范式) - 复核:gateway 包 build/vet/测试绿 + 前端 tsc 0 错;新增 12 用例(含 8 条非法输入显式码); **4 条变异反证**均已回滚 - 未做(登记):WS 帧侧的 route/detail 未在前端展示(`lib/wsParse.ts` 3 行,需放行);skill 未落 skills.Store(重启后消失,注入点在 cmd/cyrene);提醒无删除入口(CancelReminder 已存在、无路由)
This commit is contained in:
1 parent
3638ed7708
commit
decf59c1b4
12 files changed
+956
-24
No files matched your search
@@ -1,13 +1,17 @@
|
||||
package gateway
|
||||
|
||||
// 管理侧插件安装/卸载与已安装插件列表。v1 中的运行时
|
||||
// 「插件」是一个外部 MCP server:安装即连接它、把它的
|
||||
// 工具热注册进能力注册表(从而无需重启就注入下一次
|
||||
// 模型请求),把其配置持久化到被 gitignore 的
|
||||
// plugins.json,并持有活着的 server 以便之后刷新/卸载
|
||||
// (design/09 插件生态 §1)。在单静态二进制中安装编译好的 Go 插件
|
||||
// 不可能,因此不提供该方式。写操作仅 owner 且带审计;
|
||||
// API key 会被持久化,但绝不返回,也绝不原样记日志。
|
||||
// 管理侧插件安装/卸载与已安装插件列表。运行时「插件」按 design/09
|
||||
// §2.1 有两类,都经同一个 install 端点、由 body 的 `kind` 分派:
|
||||
//
|
||||
// - MCP server(本文件):安装即连接它、把它的工具热注册进能力注册表
|
||||
// (从而无需重启就注入下一次模型请求),把其配置持久化到被 gitignore
|
||||
// 的 plugins.json,并持有活着的 server 以便之后刷新/卸载。
|
||||
// - Skill(admin_plugins_skill.go):安装即解析一份工作流 YAML 并
|
||||
// RegisterSkill 进注册表。
|
||||
//
|
||||
// 在单静态二进制中安装编译好的 Go 插件不可能,因此不提供该方式。
|
||||
// 写操作仅 owner 且带审计;API key 会被持久化,但绝不返回,
|
||||
// 也绝不原样记日志。
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -36,11 +40,16 @@ type pluginActionRequest struct {
|
||||
|
||||
// pluginInstallRequest 是 POST /api/v1/admin/plugins/install 的请求体。Config
|
||||
// 承载 MCP server 连接字段;顶层的 name/kind 被单独
|
||||
// 保留,使该请求读起来与其他管理写一致。
|
||||
// 保留,使该请求读起来与其他管理写一致。YAML/Path 是
|
||||
// kind="skill" 的两种来源(内联内容或磁盘路径,二选一)。
|
||||
type pluginInstallRequest struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Config pluginInstallConfig `json:"config"`
|
||||
// YAML 是 Skill 定义的 YAML 文本(kind="skill")。
|
||||
YAML string `json:"yaml"`
|
||||
// Path 是 Skill 定义的 YAML 文件路径(kind="skill"),与 YAML 互斥。
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// pluginInstallConfig 是 MCP server 连接块。Endpoint 是 http/sse
|
||||
@@ -75,10 +84,9 @@ type AdminInstalledPlugin struct {
|
||||
ToolCount int `json:"toolCount"`
|
||||
}
|
||||
|
||||
// adminPluginInstall 安装(或刷新)一个 MCP server 插件:它校验
|
||||
// 请求、连接并发现该 server 的工具,然后持久化配置。
|
||||
// connect/discover/register 失败会返回 502 且不持久化任何东西,
|
||||
// 因此插件绝不会处于半安装状态。
|
||||
// adminPluginInstall 安装(或刷新)一个运行时插件。它先按 body 的
|
||||
// kind 分派(缺省为 mcp),两类的失败语义一致:校验或接入失败即返回
|
||||
// 400/502/409 且不留下任何半安装状态。
|
||||
func (s *Server) adminPluginInstall(w http.ResponseWriter, r *http.Request) {
|
||||
var req pluginInstallRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -86,19 +94,30 @@ func (s *Server) adminPluginInstall(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
s.auditClaims(r, "plugin.install", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "name is required"))
|
||||
return
|
||||
}
|
||||
kind := strings.TrimSpace(req.Kind)
|
||||
if kind == "" {
|
||||
kind = runtimeconfig.PluginKindMCP
|
||||
}
|
||||
if kind != runtimeconfig.PluginKindMCP {
|
||||
switch kind {
|
||||
case runtimeconfig.PluginKindMCP:
|
||||
s.adminInstallMCPPlugin(w, r, req)
|
||||
case pluginKindSkill:
|
||||
s.adminInstallSkillPlugin(w, r, req)
|
||||
default:
|
||||
s.auditClaims(r, "plugin.install", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "kind %q is not installable; only %s", kind, runtimeconfig.PluginKindMCP))
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "kind %q is not installable; only %s and %s", kind, runtimeconfig.PluginKindMCP, pluginKindSkill))
|
||||
}
|
||||
}
|
||||
|
||||
// adminInstallMCPPlugin 安装(或刷新)一个 MCP server 插件:它校验
|
||||
// 请求、连接并发现该 server 的工具,然后持久化配置。
|
||||
// connect/discover/register 失败会返回 502 且不持久化任何东西,
|
||||
// 因此插件绝不会处于半安装状态。
|
||||
func (s *Server) adminInstallMCPPlugin(w http.ResponseWriter, r *http.Request, req pluginInstallRequest) {
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
s.auditClaims(r, "plugin.install", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "name is required"))
|
||||
return
|
||||
}
|
||||
if s.plugins == nil {
|
||||
@@ -108,7 +127,7 @@ func (s *Server) adminPluginInstall(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
cfg := runtimeconfig.PluginConfig{
|
||||
Name: name,
|
||||
Kind: kind,
|
||||
Kind: runtimeconfig.PluginKindMCP,
|
||||
Transport: strings.TrimSpace(req.Config.Transport),
|
||||
Endpoint: firstNonEmpty(req.Config.Endpoint, req.Config.URL),
|
||||
Command: strings.TrimSpace(req.Config.Command),
|
||||
|
||||
Reference in new issue
Block a user