v0.8.5: Extension composability — slots, contributes, lib.require relaxation
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 5s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Successful in 2m42s
CI/CD / test-sqlite (pull_request) Successful in 2m58s
CI/CD / build-and-deploy (pull_request) Successful in 1m33s
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 5s
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-frontend (pull_request) Successful in 6s
CI/CD / test-go-pg (pull_request) Successful in 2m42s
CI/CD / test-sqlite (pull_request) Successful in 2m58s
CI/CD / build-and-deploy (pull_request) Successful in 1m33s
Manifest declarations for slots (host surfaces declare injection points) and contributes (extensions declare UI contributions). lib.require() relaxed from library-only to any package with exports. Admin slots aggregation endpoint. SDK renderAll() helper. 7 new tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,44 @@ var validTiers = map[string]bool{
|
||||
models.ExtTierSidecar: true,
|
||||
}
|
||||
|
||||
// ValidateComposabilityFields checks that manifest slots and contributes
|
||||
// fields follow the expected conventions. Slots values must have a
|
||||
// "description" string. Contributes keys must follow "pkg:slot" naming.
|
||||
// Returns nil if valid or missing; returns an error string otherwise.
|
||||
func ValidateComposabilityFields(manifest map[string]any) string {
|
||||
// Validate slots: map of name → {description, ...}
|
||||
if slotsRaw, ok := manifest["slots"]; ok {
|
||||
slots, ok := slotsRaw.(map[string]any)
|
||||
if !ok {
|
||||
return "slots must be an object"
|
||||
}
|
||||
for name, v := range slots {
|
||||
entry, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return "slots." + name + " must be an object"
|
||||
}
|
||||
if _, ok := entry["description"]; !ok {
|
||||
return "slots." + name + " must have a description"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate contributes: map of "pkg:slot" → {label, ...}
|
||||
if contribRaw, ok := manifest["contributes"]; ok {
|
||||
contribs, ok := contribRaw.(map[string]any)
|
||||
if !ok {
|
||||
return "contributes must be an object"
|
||||
}
|
||||
for key := range contribs {
|
||||
if !strings.Contains(key, ":") {
|
||||
return "contributes key " + key + " must follow pkg:slot convention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── User endpoints ──────────────────────────────
|
||||
|
||||
// ListUserExtensions returns all enabled extensions for the current user,
|
||||
@@ -208,6 +246,12 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) {
|
||||
manifestMap = map[string]any{}
|
||||
}
|
||||
|
||||
// Validate composability fields (slots / contributes)
|
||||
if errMsg := ValidateComposabilityFields(manifestMap); errMsg != "" {
|
||||
c.JSON(400, gin.H{"error": "manifest: " + errMsg})
|
||||
return
|
||||
}
|
||||
|
||||
pkg := &store.PackageRegistration{
|
||||
ID: body.ExtID,
|
||||
Title: body.Name,
|
||||
|
||||
Reference in New Issue
Block a user