ci / vet-and-test (pull_request) Successful in 46s
Why: the admin zone could not host plugin-declared partitions - KTD4 requires the RDM audit partition to be reachable by RDM domain admins and base superadmins without hardcoding RDM into the base AdminZone. Evidence: go test ./internal/app ./internal/server ./pkg/extension green incl. new admin-zone recording/endpoint tests; surface snapshot test green after surface.txt update; CI (Dev composite) is authoritative. Impact: extension surface 2.8.0 -> 2.9.0 (Registry.AddAdminZone + AdminZoneSpec, fail-closed on invalid specs); applyCompiled records declarations into the plugin summary; new authenticated GET /api/admin-zone/sections returns the flattened, ordered declaration list. Entry visibility stays metadata-only - data capability remains enforced by each section's own command surface (fail-closed). Refs: WP3-I3/KTD4; task card feature-adminzone-plugin-sections.
231 lines
7.1 KiB
Go
231 lines
7.1 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
|
||
agentcommand "agent-command"
|
||
"agent-system-backend/internal/cmdbus"
|
||
"agent-system-backend/internal/server"
|
||
"agent-system-backend/pkg/extension"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
type compiledRegistry struct {
|
||
skillsRoot string
|
||
bus *cmdbus.Bus
|
||
}
|
||
|
||
// pluginRegistry 携带插件身份与宿主能力,per-InProc 构造:
|
||
// AddCommandHandler 绑定的处理器在分发时按请求 Owner 构造 inprocHost(EXT-1)。
|
||
type pluginRegistry struct {
|
||
compiledRegistry
|
||
pluginID string
|
||
caps hostcaps
|
||
// summary 是本插件的注册面摘要(WP3-I4):AddAdminZone 声明记录于此,
|
||
// server 经 CompiledPlugins 供给分区清单端点(WP3-I3)。
|
||
summary *server.CompiledPlugin
|
||
}
|
||
|
||
// AddAdminZone 记录管理区分区声明(WP3-I3):声明即展示元数据,权威记录
|
||
// 就是注册期摘要;规格非法(非对象、id/title 缺失)fail-closed 拒绝,
|
||
// 使 register() 整体中止(与命令注册同一失败语义)。
|
||
func (r pluginRegistry) AddAdminZone(spec json.RawMessage) error {
|
||
if r.summary == nil {
|
||
return errors.New("extension: " + r.pluginID + ": admin zone outside applyCompiled")
|
||
}
|
||
var s extension.AdminZoneSpec
|
||
if err := json.Unmarshal(spec, &s); err != nil {
|
||
return fmt.Errorf("extension: admin zone spec: %w", err)
|
||
}
|
||
if s.ID == "" || s.Title == "" {
|
||
return fmt.Errorf("extension: %s: admin zone requires id and title", r.pluginID)
|
||
}
|
||
r.summary.AdminZones = append(r.summary.AdminZones, server.AdminZoneSection{
|
||
ID: s.ID, Title: s.Title, Icon: s.Icon, Desc: s.Desc, Order: s.Order,
|
||
})
|
||
return nil
|
||
}
|
||
|
||
func (r pluginRegistry) AddCommandHandler(spec json.RawMessage, h extension.Handler) error {
|
||
if r.bus == nil {
|
||
return nil
|
||
}
|
||
if h == nil {
|
||
return fmt.Errorf("extension: %s: nil command handler", r.pluginID)
|
||
}
|
||
var s agentcommand.CommandSpec
|
||
if err := json.Unmarshal(spec, &s); err != nil {
|
||
return fmt.Errorf("extension: command spec: %w", err)
|
||
}
|
||
return r.bus.RegisterPluginCommand(s, func(ctx context.Context, req cmdbus.Request) (any, error) {
|
||
host := inprocHost{caps: r.caps, pluginID: r.pluginID, owner: req.Owner, actor: req.Actor}
|
||
return h(ctx, host, extension.Request{
|
||
ID: req.ID, Params: req.Params, Entrance: req.Entrance,
|
||
Actor: req.Actor, Owner: req.Owner, ConversationID: req.ConversationID,
|
||
HostProjectID: req.HostProjectID,
|
||
})
|
||
})
|
||
}
|
||
|
||
// AddIntent 意图声明(G2-2):汇入总线意图表;插件身份随声明归因。
|
||
func (r pluginRegistry) AddIntent(spec json.RawMessage) error {
|
||
if r.bus == nil {
|
||
return nil
|
||
}
|
||
var is extension.IntentSpec
|
||
if err := json.Unmarshal(spec, &is); err != nil {
|
||
return fmt.Errorf("extension: intent spec: %w", err)
|
||
}
|
||
return r.bus.Intents().Declare(cmdbus.Intent{
|
||
ID: is.ID, Phrases: is.Phrases, Command: is.Command,
|
||
Params: is.Params, Description: is.Description, Plugin: r.pluginID,
|
||
})
|
||
}
|
||
|
||
func (r compiledRegistry) AddSkill(name, markdown string) error {
|
||
if r.skillsRoot == "" || name == "" {
|
||
return nil
|
||
}
|
||
dest := filepath.Join(r.skillsRoot, "compiled", name)
|
||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||
return err
|
||
}
|
||
return os.WriteFile(filepath.Join(dest, "SKILL.md"), []byte(markdown), 0o644)
|
||
}
|
||
|
||
func (r compiledRegistry) AddCommand(spec json.RawMessage) error {
|
||
if r.bus == nil {
|
||
return nil
|
||
}
|
||
return r.bus.RegisterPluginCommands([]json.RawMessage{spec})
|
||
}
|
||
|
||
// RegisterResourceAuthorizer 声明式扩展无宿主进程内代码,不能提供回调(fail-closed)。
|
||
func (r compiledRegistry) RegisterResourceAuthorizer(extension.ResourceAuthorizer) error {
|
||
return errors.New("extension: declarative plugins cannot register resource authorizers")
|
||
}
|
||
|
||
// RegisterResourceAuthorizer 进程内扩展(EXT-1):填充 EXT-8 适配器槽
|
||
// (hostcaps.authz 共享指针;首个注册者生效,组合根已装配进 Server/runmgr)。
|
||
// 此前 pluginRegistry 未覆盖该方法——进程内插件注册适配器会命中声明式分支的
|
||
// 拒绝,register() 整体中止,插件命令全部未注册(RDM 二进制 rdm.* 全部
|
||
// 「unknown command」;MS1 重验收烟测实测发现)。
|
||
func (r pluginRegistry) RegisterResourceAuthorizer(fn extension.ResourceAuthorizer) error {
|
||
if fn == nil {
|
||
return errors.New("extension: " + r.pluginID + ": nil resource authorizer")
|
||
}
|
||
r.caps.authz.set(r.pluginID, fn)
|
||
return nil
|
||
}
|
||
|
||
func (r compiledRegistry) Note(kind, name string, payload json.RawMessage) error {
|
||
log.Printf("extension: note %s %s (%d bytes)", kind, name, len(payload))
|
||
return nil
|
||
}
|
||
|
||
// recordRegistry 包装 per-plugin 注册表,把成功注册的声明资产名记入摘要
|
||
// (WP3-I4:注册面即清单面——编译插件的管理条目不另设声明口)。
|
||
type recordRegistry struct {
|
||
extension.Registry
|
||
sum *server.CompiledPlugin
|
||
}
|
||
|
||
func (r recordRegistry) AddCommand(spec json.RawMessage) error {
|
||
if err := r.Registry.AddCommand(spec); err != nil {
|
||
return err
|
||
}
|
||
if id := specID(spec); id != "" {
|
||
r.sum.Commands = append(r.sum.Commands, id)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r recordRegistry) AddCommandHandler(spec json.RawMessage, h extension.Handler) error {
|
||
if err := r.Registry.AddCommandHandler(spec, h); err != nil {
|
||
return err
|
||
}
|
||
if id := specID(spec); id != "" {
|
||
r.sum.Commands = append(r.sum.Commands, id)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r recordRegistry) AddIntent(spec json.RawMessage) error {
|
||
if err := r.Registry.AddIntent(spec); err != nil {
|
||
return err
|
||
}
|
||
if id := specID(spec); id != "" {
|
||
r.sum.Intents = append(r.sum.Intents, id)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r recordRegistry) AddSkill(name, markdown string) error {
|
||
if err := r.Registry.AddSkill(name, markdown); err != nil {
|
||
return err
|
||
}
|
||
r.sum.Skills = append(r.sum.Skills, name)
|
||
return nil
|
||
}
|
||
|
||
// specID 宽松提取 spec 顶层 id:仅服务清单摘要展示,解析失败返回空串
|
||
// 不记录,不影响注册结果本身。
|
||
func specID(spec json.RawMessage) string {
|
||
var s struct {
|
||
ID string `json:"id"`
|
||
}
|
||
if err := json.Unmarshal(spec, &s); err != nil {
|
||
return ""
|
||
}
|
||
return s.ID
|
||
}
|
||
|
||
func applyCompiled(skillsRoot string, bus *cmdbus.Bus, caps hostcaps) []server.CompiledPlugin {
|
||
base := compiledRegistry{skillsRoot: skillsRoot, bus: bus}
|
||
var out []server.CompiledPlugin
|
||
for _, e := range extension.Compiled() {
|
||
if e.Register == nil {
|
||
continue
|
||
}
|
||
sum := server.CompiledPlugin{ID: e.ID}
|
||
reg := pluginRegistry{compiledRegistry: base, pluginID: e.ID, caps: caps, summary: &sum}
|
||
// Register 失败的插件保留已成功注册部分(与命令总线一致:部分注册
|
||
// 生效),摘要如实反映实际注册面。
|
||
if err := e.Register(recordRegistry{Registry: reg, sum: &sum}); err != nil {
|
||
log.Printf("extension: %s: %v", e.ID, err)
|
||
}
|
||
out = append(out, sum)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mergeMCPServers(raw string, extra []core.PluginMCPServer) string {
|
||
var base []json.RawMessage
|
||
if raw != "" {
|
||
if err := json.Unmarshal([]byte(raw), &base); err != nil {
|
||
return raw
|
||
}
|
||
}
|
||
for _, s := range extra {
|
||
b, err := json.Marshal(s)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
base = append(base, b)
|
||
}
|
||
if len(base) == 0 {
|
||
return ""
|
||
}
|
||
out, err := json.Marshal(base)
|
||
if err != nil {
|
||
return raw
|
||
}
|
||
return string(out)
|
||
}
|