V0.38.2 library packages (#235)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -13,8 +13,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.starlark.net/starlark"
|
||||
|
||||
"chat-switchboard/database"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/sandbox"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
@@ -29,6 +31,7 @@ type PackageHandler struct {
|
||||
stores store.Stores
|
||||
packagesDir string // e.g. /data/packages — where archives are extracted
|
||||
sandbox *sandbox.Sandbox // v0.30.0: for schema migration scripts
|
||||
runner *sandbox.Runner // v0.38.2: for test-tool execution
|
||||
}
|
||||
|
||||
func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
|
||||
@@ -44,6 +47,11 @@ func (h *PackageHandler) SetSandbox(sb *sandbox.Sandbox) {
|
||||
h.sandbox = sb
|
||||
}
|
||||
|
||||
// SetRunner attaches the Starlark runner for test-tool execution (v0.38.2).
|
||||
func (h *PackageHandler) SetRunner(r *sandbox.Runner) {
|
||||
h.runner = r
|
||||
}
|
||||
|
||||
// ListPackages returns all registered packages.
|
||||
// GET /api/v1/admin/packages
|
||||
// GET /api/v1/admin/surfaces (alias)
|
||||
@@ -126,6 +134,20 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// v0.38.2: Libraries with active consumers cannot be uninstalled.
|
||||
if pkg.Type == "library" && h.stores.Dependencies != nil {
|
||||
hasConsumers, _ := h.stores.Dependencies.HasConsumers(c.Request.Context(), id)
|
||||
if hasConsumers {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "cannot uninstall: library has active consumers"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// v0.38.2: Clean up dependency records when uninstalling a consumer.
|
||||
if h.stores.Dependencies != nil {
|
||||
h.stores.Dependencies.DeleteAllForConsumer(c.Request.Context(), id)
|
||||
}
|
||||
|
||||
// v0.29.2: Drop namespaced DB tables before removing the package record.
|
||||
if err := DropExtTables(c.Request.Context(), database.DB, h.stores, id); err != nil {
|
||||
log.Printf("[ext-db] schema drop failed for %s: %v", id, err)
|
||||
@@ -281,8 +303,8 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
if pkgType == "" {
|
||||
pkgType = "surface"
|
||||
}
|
||||
if pkgType != "surface" && pkgType != "extension" && pkgType != "full" && pkgType != "workflow" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "manifest type must be 'surface', 'extension', 'full', or 'workflow'"})
|
||||
if pkgType != "surface" && pkgType != "extension" && pkgType != "full" && pkgType != "workflow" && pkgType != "library" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "manifest type must be 'surface', 'extension', 'full', 'workflow', or 'library'"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -323,6 +345,21 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow packages require a 'workflow_definition' in the manifest"})
|
||||
return
|
||||
}
|
||||
case "library":
|
||||
// v0.38.2: Libraries must declare exports, cannot have tools/pipes/route.
|
||||
exports, _ := manifest["exports"].([]any)
|
||||
if len(exports) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages require an 'exports' array"})
|
||||
return
|
||||
}
|
||||
if hasTools || hasPipes {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages cannot have tools or pipes"})
|
||||
return
|
||||
}
|
||||
if hasRoute {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "library packages cannot have a route"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conflicts with core packages
|
||||
@@ -470,6 +507,47 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.38.2: Create dependency records from manifest "dependencies" map.
|
||||
// Libraries must be installed first; consumers declare them.
|
||||
if deps, ok := manifest["dependencies"].(map[string]any); ok && len(deps) > 0 {
|
||||
// Clear stale dependency records from a previous install of the same consumer.
|
||||
if h.stores.Dependencies != nil {
|
||||
h.stores.Dependencies.DeleteAllForConsumer(c.Request.Context(), pkgID)
|
||||
}
|
||||
for libID, vSpec := range deps {
|
||||
lib, err := h.stores.Packages.Get(c.Request.Context(), libID)
|
||||
if err != nil || lib == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency not found: " + libID})
|
||||
return
|
||||
}
|
||||
if lib.Type != "library" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency is not a library: " + libID})
|
||||
return
|
||||
}
|
||||
if lib.Status != "active" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency is not active: " + libID})
|
||||
return
|
||||
}
|
||||
versionSpec, _ := vSpec.(string)
|
||||
if versionSpec == "" {
|
||||
versionSpec = ">=0.0.0"
|
||||
}
|
||||
if h.stores.Dependencies != nil {
|
||||
if err := h.stores.Dependencies.Create(c.Request.Context(), &models.ExtDependency{
|
||||
ConsumerID: pkgID,
|
||||
LibraryID: libID,
|
||||
VersionSpec: versionSpec,
|
||||
ResolvedVer: lib.Version,
|
||||
}); err != nil {
|
||||
log.Printf("[packages] dependency record failed for %s → %s: %v", pkgID, libID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dependency record"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[packages] %s: %d dependencies recorded", pkgID, len(deps))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": pkgID,
|
||||
"title": title,
|
||||
@@ -650,3 +728,78 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": enabled})
|
||||
}
|
||||
|
||||
// ── Test Tool (v0.38.2) ─────────────────────────
|
||||
// POST /admin/packages/:id/test-tool
|
||||
// Invokes a starlark extension's on_tool_call entry point directly,
|
||||
// without going through the AI chat loop. Admin-only test harness.
|
||||
|
||||
// TestToolRequest is the JSON body for test-tool invocation.
|
||||
type TestToolRequest struct {
|
||||
ToolName string `json:"tool_name" binding:"required"`
|
||||
Arguments map[string]any `json:"arguments"`
|
||||
}
|
||||
|
||||
// TestTool invokes a package's on_tool_call entry point for testing.
|
||||
func (h *PackageHandler) TestTool(c *gin.Context) {
|
||||
if h.runner == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "starlark runner not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
pkgID := c.Param("id")
|
||||
pkg, err := h.stores.Packages.Get(c.Request.Context(), pkgID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
|
||||
return
|
||||
}
|
||||
if pkg.Tier != "starlark" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "test-tool requires tier=starlark"})
|
||||
return
|
||||
}
|
||||
if !pkg.Enabled {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "package is disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
var req TestToolRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Build a call dict identical to what executeExtensionTool sends.
|
||||
// Build the arguments as an interface{} map for jsonToStarlark.
|
||||
var args interface{}
|
||||
if req.Arguments != nil {
|
||||
argsJSON, _ := json.Marshal(req.Arguments)
|
||||
_ = json.Unmarshal(argsJSON, &args)
|
||||
}
|
||||
|
||||
callDict := starlark.NewDict(3)
|
||||
_ = callDict.SetKey(starlark.String("tool_name"), starlark.String(req.ToolName))
|
||||
_ = callDict.SetKey(starlark.String("tool_call_id"), starlark.String("test-"+pkgID))
|
||||
_ = callDict.SetKey(starlark.String("arguments"), jsonToStarlark(args))
|
||||
|
||||
userID, _ := c.Get("user_id")
|
||||
rc := &sandbox.RunContext{UserID: fmt.Sprintf("%v", userID)}
|
||||
|
||||
val, output, err := h.runner.CallEntryPoint(
|
||||
c.Request.Context(), pkg, "on_tool_call",
|
||||
starlark.Tuple{callDict}, nil, rc,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": err.Error(),
|
||||
"output": output,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Serialize the Starlark return value to Go types.
|
||||
result := starlarkValueToGo(val)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"result": result,
|
||||
"output": output,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user