Files
Agent-System-Backend/internal/app/compiled.go
T
weichen 0270b40197
ci / vet-and-test (pull_request) Successful in 45s
Add EXT-8b resource-level authorization contract
Why: 方案 r1 U9 首切片——V1(owner×project 布尔)无法表达资源粒度判定,读面(下载/列表/检索/模型上下文)需要执行端按资源咨询下游。
Evidence: 红:resource_authz_v2_test.go 编译红(ResourceAuthzQuery/V2 未定义);绿:pkg/extension surface 棘轮(2.10.0)+ internal/app 槽位测试(V2 咨询/V1 回落/字段缺失拒/error fail-closed/首注册者生效/compiled 拒绝);go test ./... 全绿(收据 /tmp/be-v2-test.txt)。
Impact: 公开面新增 ResourceAuthzQuery/ResourceAuthorizerV2/Registry.RegisterResourceAuthorizerV2;projectAuthz 增 V2 槽(AuthorizeResource:V2 优先、V1 兼容回落、受信字段缺失=拒);compiledRegistry fail-closed 拒绝声明式注册;读路径消费接线(server 5 处 authorizeSharedProject 升级)留待下一切片。
Refs: 方案 r1 U9/KTD2;RDM EXT-8 现有消费者(hostcaps/projectAuthz)
2026-10-12 11:44:00 +00:00

245 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)。
// RegisterResourceAuthorizerV2 声明式扩展同样不能提供资源级回调(fail-closed)。
func (r compiledRegistry) RegisterResourceAuthorizerV2(extension.ResourceAuthorizerV2) error {
return errors.New("extension: declarative plugins cannot register resource authorizers")
}
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
}
// RegisterResourceAuthorizerV2 进程内扩展:填充 EXT-8b 资源级适配器槽(首注册生效)。
func (r pluginRegistry) RegisterResourceAuthorizerV2(fn extension.ResourceAuthorizerV2) error {
if fn == nil {
return errors.New("extension: " + r.pluginID + ": nil resource authorizer v2")
}
r.caps.authz.setV2(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)
}