Initial commit: Cyrene Plugins SDK + Plugin Manager
Extracted from Cyrene main repo (backend/pkg/plugins + backend/plugin-manager). Contains SDK interfaces (Plugin/Tool/HostAPI), 13 built-in plugins, ToolRegistry with call log ring buffer, and Plugin Manager REST API service. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene-Plugins/manager"
|
||||
)
|
||||
|
||||
// PluginHandler exposes the Plugin Manager REST API via net/http.
|
||||
type PluginHandler struct {
|
||||
mgr *manager.PluginManager
|
||||
}
|
||||
|
||||
func NewPluginHandler(mgr *manager.PluginManager) *PluginHandler {
|
||||
return &PluginHandler{mgr: mgr}
|
||||
}
|
||||
|
||||
func (h *PluginHandler) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/plugins", h.listPlugins)
|
||||
mux.HandleFunc("/api/v1/plugins/", h.pluginRoute)
|
||||
mux.HandleFunc("/api/v1/tools", h.listTools)
|
||||
mux.HandleFunc("/api/v1/tools/", h.toolRoute)
|
||||
mux.HandleFunc("/api/v1/health", h.health)
|
||||
}
|
||||
|
||||
func (h *PluginHandler) health(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "ok", "service": "plugin-manager"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) listPlugins(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
plugins := h.mgr.List()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"plugins": plugins, "total": len(plugins)})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) pluginRoute(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/plugins/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
pluginID := parts[0]
|
||||
|
||||
if pluginID == "" {
|
||||
h.listPlugins(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
h.getPlugin(w, pluginID)
|
||||
case "DELETE":
|
||||
h.uninstallPlugin(w, r, pluginID)
|
||||
default:
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
action := parts[1]
|
||||
switch action {
|
||||
case "enable":
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.enablePlugin(w, r, pluginID)
|
||||
case "disable":
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.disablePlugin(w, r, pluginID)
|
||||
case "reload":
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.reloadPlugin(w, r, pluginID)
|
||||
case "tools":
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.pluginTools(w, pluginID)
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, errResp("not found"))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PluginHandler) getPlugin(w http.ResponseWriter, id string) {
|
||||
info, ok := h.mgr.Get(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, errResp("plugin not found"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, info)
|
||||
}
|
||||
|
||||
func (h *PluginHandler) enablePlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Enable(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "enabled"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) disablePlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Disable(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "disabled"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) reloadPlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Reload(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "reloaded"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) uninstallPlugin(w http.ResponseWriter, r *http.Request, id string) {
|
||||
if err := h.mgr.Uninstall(r.Context(), id); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "uninstalled"})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) pluginTools(w http.ResponseWriter, id string) {
|
||||
info, ok := h.mgr.Get(id)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, errResp("plugin not found"))
|
||||
return
|
||||
}
|
||||
registry := h.mgr.Registry()
|
||||
tools := make([]interface{}, 0)
|
||||
for _, toolID := range info.Tools {
|
||||
if t, ok := registry.Get(toolID); ok {
|
||||
tools = append(tools, t.Definition())
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": tools, "total": len(tools)})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) listTools(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
defs := h.mgr.Registry().Definitions()
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"tools": defs, "total": len(defs)})
|
||||
}
|
||||
|
||||
func (h *PluginHandler) toolRoute(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/tools/")
|
||||
toolID := path
|
||||
|
||||
if strings.HasSuffix(path, "/execute") {
|
||||
toolID = strings.TrimSuffix(path, "/execute")
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
h.executeTool(w, r, toolID)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, http.StatusMethodNotAllowed, errResp("method not allowed"))
|
||||
return
|
||||
}
|
||||
tool, ok := h.mgr.Registry().Get(toolID)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, errResp("tool not found"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tool.Definition())
|
||||
}
|
||||
|
||||
func (h *PluginHandler) executeTool(w http.ResponseWriter, r *http.Request, toolID string) {
|
||||
var body struct {
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, errResp("invalid request body"))
|
||||
return
|
||||
}
|
||||
result, err := h.mgr.Registry().Execute(r.Context(), toolID, body.Arguments)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, errResp(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func errResp(msg string) map[string]string {
|
||||
return map[string]string{"error": msg}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
Reference in New Issue
Block a user