package plugins import ( "encoding/json" "fmt" "net/url" "os" "os/exec" "path/filepath" "strings" "agent-system-backend/internal/skills" "agent-system-backend/internal/store" "agent-system-backend/internal/tools" core "agent-system-core" ) func (h *Host) SetSkillsRoot(dir string) { h.skillsRoot = dir } func (h *Host) SetHostAPI(baseURL, token string) { h.hostAPI, h.hostToken = baseURL, token } func (h *Host) startSidecar(m core.PluginManifest, dir string) error { if m.Backend.Mode != "sidecar" || m.Backend.Command == "" { return nil } cmdPath := m.Backend.Command if !filepath.IsAbs(cmdPath) { if _, err := os.Stat(filepath.Join(dir, cmdPath)); err == nil { cmdPath = filepath.Join(dir, cmdPath) } } cmd := exec.Command(cmdPath, m.Backend.Args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "AGENT_HOST_API="+h.hostAPI, "AGENT_HOST_TOKEN="+h.hostToken, "AGENT_PLUGIN_ID="+m.ID, ) if err := cmd.Start(); err != nil { return fmt.Errorf("plugins: %s: sidecar: %w", m.ID, err) } h.mu.Lock() if h.sidecars == nil { h.sidecars = map[string]*exec.Cmd{} } if prev := h.sidecars[m.ID]; prev != nil && prev.Process != nil { _ = prev.Process.Kill() } h.sidecars[m.ID] = cmd h.mu.Unlock() go func() { _ = cmd.Wait() }() return nil } func (h *Host) loadDeclared(dir string, m core.PluginManifest, ld *Loaded) error { for _, name := range m.Provides.Skills { body, err := os.ReadFile(filepath.Join(dir, "skills", name+".md")) if err != nil { return fmt.Errorf("plugins: %s: skill %s: %w", m.ID, name, err) } // 技能按开放规范校验(N2 WP4):不合规整批拒载,字段定位到键。 if err := skills.ValidateForPlugin(m.ID, name, body); err != nil { return err } if ld.Skills == nil { ld.Skills = map[string]string{} } ld.Skills[name] = string(body) if h.skillsRoot != "" { dest := filepath.Join(h.skillsRoot, "plugin-"+m.ID, name) if err := os.MkdirAll(dest, 0o755); err != nil { return err } if err := os.WriteFile(filepath.Join(dest, "SKILL.md"), body, 0o644); err != nil { return err } } } for _, name := range m.Provides.MCPServers { var srv core.PluginMCPServer if err := readAsset(dir, "mcp", name, &srv); err != nil { return fmt.Errorf("plugins: %s: mcp %s: %w", m.ID, name, err) } if srv.Name == "" { srv.Name = name } ld.MCP = append(ld.MCP, srv) } for _, name := range m.Provides.Knowledge { var item core.PluginKnowledgeSet if err := readAsset(dir, "knowledge", name, &item); err != nil { return fmt.Errorf("plugins: %s: knowledge %s: %w", m.ID, name, err) } ld.Knowledge = append(ld.Knowledge, item) } for _, name := range m.Provides.Parsers { var item core.PluginParserDecl if err := readAsset(dir, "parsers", name, &item); err != nil { return fmt.Errorf("plugins: %s: parser %s: %w", m.ID, name, err) } ld.Parsers = append(ld.Parsers, item) } for _, name := range m.Provides.Agents { raw, err := readPluginFile(dir, filepath.Join("agents", name+".json")) if err != nil { return fmt.Errorf("plugins: %s: agent %s: %w", m.ID, name, err) } item, err := parseAgentAsset(raw) if err != nil { return fmt.Errorf("plugins: %s: agent %s: %w", m.ID, name, err) } ld.Agents = append(ld.Agents, item) } for _, name := range m.Provides.Channels { var item core.PluginChannelDecl if err := readAsset(dir, "channels", name, &item); err != nil { return fmt.Errorf("plugins: %s: channel %s: %w", m.ID, name, err) } ld.Channels = append(ld.Channels, item) } for _, name := range m.Provides.Pages { var item core.PluginPageDecl if err := readAsset(dir, "pages", name, &item); err != nil { return fmt.Errorf("plugins: %s: page %s: %w", m.ID, name, err) } if item.Module == "" { return fmt.Errorf("plugins: %s: page %s: missing module", m.ID, name) } ld.Pages = append(ld.Pages, item) } if m.Backend.SchemaVersion > 0 && h.st != nil { cur, err := h.st.PluginSchemaVersion(m.ID) if err != nil { return err } if m.Backend.SchemaVersion < cur { return fmt.Errorf("plugins: %s: schema %d older than stored %d", m.ID, m.Backend.SchemaVersion, cur) } if err := h.st.SetPluginSchemaVersion(m.ID, m.Backend.SchemaVersion); err != nil { return err } } return nil } // MCPServers 返回已装载插件贡献的 MCP 服务,供与 settings 合并。 func (h *Host) MCPServers() []core.PluginMCPServer { h.mu.RLock() defer h.mu.RUnlock() var out []core.PluginMCPServer for _, ld := range h.loaded { out = append(out, ld.MCP...) } return out } // ListFor 按租户授权与 manifest 作用域过滤。project 为空时只检查租户。 func (h *Host) ListFor(owner, project string) []*Loaded { all := h.List() var out []*Loaded for _, ld := range all { if !ld.Manifest.Scope.Allows(owner, project) { continue } if h.st != nil && owner != "" { on, err := h.st.PluginEnabled(owner, ld.Manifest.ID) if err != nil || !on { continue } } out = append(out, ld) } return out } func (h *Host) bindToolExecutor(desc core.ToolDesc, apiVersion string, allow []string) (tools.Executor, error) { if desc.HTTP == nil { return nil, fmt.Errorf("v1 仅支持声明式 HTTP 工具(缺少 http 段)") } if desc.HTTP.URL == "" || desc.HTTP.Method == "" { return nil, fmt.Errorf("http 工具缺少 url/method") } if err := checkNetwork(apiVersion, allow, desc.HTTP.URL); err != nil { return nil, err } return func(rc tools.ToolExecCtx, args json.RawMessage) (string, error) { if err := checkNetwork(apiVersion, allow, desc.HTTP.URL); err != nil { return "", err } owner := store.OwnerDefault if rc != nil && rc.OwnerID() != "" { owner = rc.OwnerID() } lookup := func(key string) string { eff := h.settings.Effective(owner, "") if desc.PluginID != "" { if v := eff.GetString("secrets."+desc.PluginID+"."+key, ""); v != "" { return v } } return eff.GetString(key, "") } return tools.HTTPExecutor(*desc.HTTP, lookup)(rc, args) }, nil } func checkNetwork(apiVersion string, allow []string, rawURL string) error { if len(allow) == 0 { if apiVersion == core.PluginAPIVersionExperimental { return fmt.Errorf("plugins: api %s 要求 requires.network", apiVersion) } return nil } u, err := url.Parse(rawURL) if err != nil { return fmt.Errorf("plugins: tool url: %w", err) } host := u.Hostname() for _, a := range allow { a = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(a)), "*.") h := strings.ToLower(host) if h == a || strings.HasSuffix(h, "."+a) { return nil } } return fmt.Errorf("plugins: host %s not in network allowlist", host) }