Feat v0.6.6 final hardening (#41)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / test-sqlite (push) Successful in 3m1s
CI/CD / build-and-deploy (push) Successful in 29s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #41.
This commit is contained in:
2026-03-31 17:40:40 +00:00
committed by xcaliber
parent 81c28a50bf
commit 7915d84c8b
19 changed files with 625 additions and 131 deletions

View File

@@ -29,10 +29,11 @@ var validPackageID = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$`)
// PackageHandler manages unified package lifecycle (admin-only).
// Replaces SurfaceHandler.
type PackageHandler struct {
stores store.Stores
packagesDir string // e.g. /data/packages — where archives are extracted
sandbox *sandbox.Sandbox
runner *sandbox.Runner
stores store.Stores
packagesDir string // e.g. /data/packages — where archives are extracted
bundledDir string // e.g. /app/bundled-packages — .pkg archive source
sandbox *sandbox.Sandbox
runner *sandbox.Runner
}
func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
@@ -43,6 +44,12 @@ func NewPackageHandler(s store.Stores, packagesDir ...string) *PackageHandler {
return &PackageHandler{stores: s, packagesDir: dir}
}
// SetBundledDir sets the path to the bundled packages directory.
// Used by dependency auto-activation to resolve missing dependencies.
func (h *PackageHandler) SetBundledDir(dir string) {
h.bundledDir = dir
}
// SetSandbox attaches a Starlark sandbox for schema migrations.
func (h *PackageHandler) SetSandbox(sb *sandbox.Sandbox) {
h.sandbox = sb
@@ -319,79 +326,22 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
}
}
// Validate required fields
pkgID, _ := manifest["id"].(string)
title, _ := manifest["title"].(string)
if pkgID == "" || title == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "manifest must have 'id' and 'title'"})
// Validate manifest structure (required fields, type, constraints)
mInfo, err := ValidateManifest(manifest)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pkgID := mInfo.ID
title := mInfo.Title
pkgType := mInfo.Type
// Validate package ID is a safe slug
if !validPackageID.MatchString(pkgID) {
c.JSON(http.StatusBadRequest, gin.H{"error": "package id must be a lowercase slug (a-z, 0-9, hyphens, 2-64 chars)"})
return
}
// Determine type (default: surface for backward compat)
pkgType, _ := manifest["type"].(string)
if pkgType == "" {
pkgType = "surface"
}
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
}
// Type-specific validation
hasRoute := manifest["route"] != nil || manifest["routes"] != nil
hasTools := manifest["tools"] != nil
hasPipes := manifest["pipes"] != nil
hasHooks := manifest["hooks"] != nil
hasSettings := manifest["settings"] != nil
hasExtBehavior := hasTools || hasPipes || hasHooks
switch pkgType {
case "surface":
// route is optional for surfaces (core surfaces don't always have it in manifest)
case "extension":
if !hasExtBehavior {
c.JSON(http.StatusBadRequest, gin.H{"error": "extension packages require at least one of: tools, pipes, hooks"})
return
}
if hasRoute {
c.JSON(http.StatusBadRequest, gin.H{"error": "extension packages cannot have a route — use type 'full' for both"})
return
}
case "full":
if !hasRoute {
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require a route"})
return
}
// or a settings schema. A surface-with-settings is a valid "full" package
// (e.g. editor: browser-tier surface + admin-configurable settings).
if !hasExtBehavior && !hasSettings {
c.JSON(http.StatusBadRequest, gin.H{"error": "full packages require at least one of: tools, pipes, hooks, settings"})
return
}
case "workflow":
if manifest["workflow_definition"] == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow packages require a 'workflow_definition' in the manifest"})
return
}
case "library":
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
// Package signing hook (schema reserved — no crypto verification yet)
if os.Getenv("PACKAGE_VERIFY_SIGNATURES") == "true" {
if mInfo.Signature != "" {
log.Printf("[packages] %s: signature field present (%s) — verification not yet implemented", pkgID, mInfo.Signature)
} else {
log.Printf("[packages] %s: unsigned package (PACKAGE_VERIFY_SIGNATURES is enabled)", pkgID)
}
}
@@ -443,17 +393,11 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
log.Printf("[packages] Extracted %s to %s", pkgID, destDir)
}
// Extract manifest fields for DB columns
version, _ := manifest["version"].(string)
if version == "" {
version = "0.0.0"
}
description, _ := manifest["description"].(string)
author, _ := manifest["author"].(string)
tier, _ := manifest["tier"].(string)
if tier == "" {
tier = "browser"
}
// Use validated manifest fields for DB columns
version := mInfo.Version
description := mInfo.Description
author := mInfo.Author
tier := mInfo.Tier
userID := c.GetString("user_id")
@@ -543,6 +487,7 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
}
// Libraries must be installed first; consumers declare them.
// If a dependency is missing but available as a bundled package, auto-install it.
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 {
@@ -551,8 +496,25 @@ func (h *PackageHandler) InstallPackage(c *gin.Context) {
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
// Attempt auto-install from bundled packages
if h.bundledDir != "" {
pkgPath := filepath.Join(h.bundledDir, libID+".pkg")
if _, statErr := os.Stat(pkgPath); statErr == nil {
log.Printf("[packages] auto-installing bundled dependency %s for %s", libID, pkgID)
if _, installErr := installBundledPackage(pkgPath, h.packagesDir, h.stores, h.runner); installErr != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("dependency %s auto-install failed: %v", libID, installErr),
})
return
}
// Re-fetch after install
lib, err = h.stores.Packages.Get(c.Request.Context(), libID)
}
}
if lib == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency not found: " + libID + " (not installed and not available as a bundled package)"})
return
}
}
if lib.Type != "library" {
c.JSON(http.StatusBadRequest, gin.H{"error": "dependency is not a library: " + libID})