Files
heranran 71c7672545 feat: Skill Hub hardening with session usage tracking and egress governance (#163)
* feat(skill-hub): add Skill Hub catalog, publish flow, and lite materialize pipeline

Introduce Skill Hub for browsing, importing, publishing, and installing skills, with lite instance package materialization and runtime sync support.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): remove token-governance hooks from Skill Hub PR

Strip validateManagedRuntimeEnvironmentOverrides, network lock policy sync,
and egress proxy audit wiring that belong to the upcoming token-usage work,
so the Skill Hub branch compiles independently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): update migration number in materialize docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): make RuntimeAgentClient test stub and hub tests compile-safe

Add ResyncInstanceSkills to the runtime pool handler fake client, and harden
skill hub payload helpers/tests against nil storage/instance repos so go test passes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add Skill Hub hardening with session usage tracking and egress governance

Unify Skill Hub runtime sync improvements with session-token observability,
egress network policy, and admin/instance usage reporting for reopenable PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): repair CI tests and nested skill install

* fix(ci): restore release deployment configuration

---------

Co-authored-by: heshengran <heshengran@ieisystem.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:37:01 +08:00

121 lines
7.4 KiB
Go

package utils
import (
"errors"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// Success sends a successful response
func Success(c *gin.Context, status int, message string, data interface{}) {
c.JSON(status, gin.H{
"success": true,
"message": message,
"data": data,
})
}
// Error sends an error response
func Error(c *gin.Context, status int, message string) {
c.JSON(status, gin.H{
"success": false,
"error": message,
})
}
// HandleError handles different types of errors and sends appropriate responses
func HandleError(c *gin.Context, err error) {
// Log the actual error for debugging
log.Printf("[ERROR] %v", err)
// Handle validation errors
if validationErrors, ok := err.(validator.ValidationErrors); ok {
Error(c, http.StatusBadRequest, formatValidationErrors(validationErrors))
return
}
// Handle known errors
errStr := err.Error()
if strings.HasPrefix(errStr, "provider discovery failed:") || strings.HasPrefix(errStr, "failed to call provider discovery endpoint:") || strings.HasPrefix(errStr, "failed to decode provider discovery response:") {
Error(c, http.StatusBadGateway, errStr)
return
}
if strings.HasPrefix(errStr, "failed to get secret ") || strings.HasPrefix(errStr, "secret key ") || strings.HasPrefix(errStr, "secret value is empty") {
Error(c, http.StatusBadGateway, errStr)
return
}
if strings.HasPrefix(errStr, "missing required openclaw config dependency:") || strings.HasPrefix(errStr, "required openclaw config dependency is disabled:") {
Error(c, http.StatusBadRequest, errStr)
return
}
if strings.HasPrefix(errStr, "duplicate team member id:") || strings.HasPrefix(errStr, "redis url is invalid:") || strings.HasPrefix(errStr, "redis db index is invalid:") || strings.HasPrefix(errStr, "instance limit reached:") || strings.HasPrefix(errStr, "CPU cores exceed quota:") || strings.HasPrefix(errStr, "memory exceed quota:") || strings.HasPrefix(errStr, "storage exceed quota:") || strings.HasPrefix(errStr, "GPU count exceed quota:") {
Error(c, http.StatusBadRequest, errStr)
return
}
switch errStr {
case "username already exists", "email already exists", "instance name already exists", "team name already exists", "openclaw config resource key already exists", "team task message id already exists":
Error(c, http.StatusConflict, errStr)
case "display name already exists":
Error(c, http.StatusConflict, errStr)
case "unsupported instance type", "instance name is required", "image is required", "display name is required", "provider type is required", "base URL is required", "provider model name is required", "input price must be non-negative", "output price must be non-negative", "base URL is invalid", "automatic model discovery for azure-openai is not supported yet", "provider discovery is not supported", "model is required", "messages are required", "streaming is not supported yet", "provider type is not supported yet", "trace id is required", "event type is required", "message is required", "risk hit record is incomplete", "rule id is required", "rule display name is required", "rule pattern is required", "rule pattern is invalid", "risk severity is invalid", "risk action is invalid", "sample text is required", "secret ref format is invalid", "secret namespace is required in secret ref", "invalid openclaw resource type", "invalid openclaw config plan mode", "openclaw config resource name is required", "openclaw config resource key is invalid", "openclaw config schemaVersion is required", "openclaw config kind does not match resource type", "openclaw config format is required", "openclaw config content is required", "openclaw config content must be valid JSON", "openclaw config config payload is required", "openclaw config dependency type is invalid", "openclaw config dependency key is required", "openclaw config dependency is invalid", "openclaw config bundle name is required", "openclaw config bundle must include at least one resource", "openclaw config bundle resource id is required", "openclaw config bundle contains duplicate resources", "openclaw config bundle is required", "openclaw config bundle is disabled", "openclaw config bundle is empty", "at least one openclaw config resource must be selected", "openclaw config resource id is invalid", "openclaw config resource is disabled", "openclaw config bundle contains a disabled resource", "openclaw bootstrap payload is too large", "agent bootstrap token is required", "agent id is required", "unsupported agent protocol version", "invalid agent bootstrap token", "invalid instance command type", "invalid instance command finish status", "team name is required", "team must include at least one member", "team must include exactly one leader", "team redis url is required", "redis url scheme must be redis or rediss", "team member id is invalid", "target member id is required", "task payload is required", "team leader cannot be deleted before assigning a new leader":
Error(c, http.StatusBadRequest, errStr)
case "model is not active or does not exist", "openclaw config resource not found", "openclaw config bundle not found", "openclaw injection snapshot not found", "instance command not found", "instance config revision not found", "team not found", "team member not found":
Error(c, http.StatusNotFound, errStr)
case "risk rule not found":
Error(c, http.StatusNotFound, errStr)
case "sensitive content requires an active secure model", "request was blocked by risk policy":
Error(c, http.StatusForbidden, errStr)
case "invalid username or password", "account is disabled", "invalid or expired agent session token":
Error(c, http.StatusUnauthorized, errStr)
case "agent registration is only supported for openclaw instances", "agent registration is only supported for openclaw or hermes instances", "agent id does not match session", "access denied", "skill_attach_forbidden":
Error(c, http.StatusForbidden, errStr)
case "current password is incorrect":
Error(c, http.StatusBadRequest, errStr)
case "user not found", "model not found", "skill not found", "skill hub tag not found":
Error(c, http.StatusNotFound, errStr)
case "skill_not_scanned", "skill_risk_blocked", "skill_tags_required", "skill is not published to hub":
Error(c, http.StatusBadRequest, errStr)
case "skill_package_pending", "skill_package_materialize_failed", "skill_package_materializing":
Error(c, http.StatusConflict, err.Error())
default:
// For development, show actual error; for production, hide details
Error(c, http.StatusInternalServerError, errStr)
}
}
// ValidationError handles validation errors from gin binding
func ValidationError(c *gin.Context, err error) {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
Error(c, http.StatusBadRequest, formatValidationErrors(ve))
return
}
Error(c, http.StatusBadRequest, err.Error())
}
func formatValidationErrors(errs validator.ValidationErrors) string {
var messages []string
for _, err := range errs {
switch err.Tag() {
case "required":
messages = append(messages, err.Field()+" is required")
case "min":
messages = append(messages, err.Field()+" must be at least "+err.Param()+" characters")
case "max":
messages = append(messages, err.Field()+" must be at most "+err.Param()+" characters")
case "email":
messages = append(messages, err.Field()+" must be a valid email")
case "alphanum":
messages = append(messages, err.Field()+" must be alphanumeric")
default:
messages = append(messages, err.Field()+" is invalid")
}
}
return messages[0]
}