Files
attesto-go/connectorkit/manifest.go
2026-06-09 17:52:15 +02:00

271 lines
10 KiB
Go

package connectorkit
import "regexp"
type Manifest struct {
SchemaVersion string `json:"schemaVersion"`
Slug string `json:"slug"`
Name string `json:"name"`
Version string `json:"version"`
AssetType string `json:"assetType"`
Category string `json:"category"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Publisher map[string]string `json:"publisher"`
Repository map[string]string `json:"repository"`
Documentation map[string]string `json:"documentation"`
Capabilities []string `json:"capabilities"`
Evidence map[string]bool `json:"evidence"`
Security map[string]bool `json:"security"`
SupportedLanguages []string `json:"supportedLanguages,omitempty"`
Provider map[string]any `json:"provider,omitempty"`
Auth map[string]any `json:"auth,omitempty"`
Sync map[string]any `json:"sync,omitempty"`
EventTypes []string `json:"eventTypes,omitempty"`
SourceTime map[string]any `json:"sourceTime,omitempty"`
ConfigSchema map[string]any `json:"configSchema,omitempty"`
SecretSchema map[string]any `json:"secretSchema,omitempty"`
Diagnostics map[string]any `json:"diagnostics,omitempty"`
Runtime map[string]any `json:"runtime,omitempty"`
InstallRequirements map[string]any `json:"installRequirements,omitempty"`
Changelog []map[string]any `json:"changelog,omitempty"`
}
type Finding struct {
Code string
Severity string
Message string
}
type ValidationResult struct {
OK bool
EvidenceScore int
Tier string
Findings []Finding
}
var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,95}$`)
var versionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$`)
var categories = map[string]bool{
"ai-governance": true,
"compliance": true,
"crm": true,
"devops": true,
"erp": true,
"iam": true,
"security": true,
"storage": true,
}
var schemaVersions = map[string]bool{"1.0": true, "attesto.connector.v2": true}
var supportedAuthModes = map[string]bool{
"api-key": true, "basic-auth": true, "oauth2": true, "signed-webhook": true,
"storage-credentials": true, "token": true,
}
var supportedSyncModes = map[string]bool{
"manual": true, "object-commit": true, "polling": true, "webhook": true,
}
var requiredDiagnostics = []string{
"providerAuthStatus", "replayConflictCheck", "revocationCheck", "syncLag", "testConnection",
}
var requiredRuntimeMethods = []string{
"diagnostics", "emitProofstreamEvent", "handleWebhook", "metadata", "revoke", "sync", "testConnection", "validateConfig",
}
func ValidateManifest(manifest Manifest) ValidationResult {
findings := make([]Finding, 0)
if !slugPattern.MatchString(manifest.Slug) {
findings = append(findings, Finding{"manifest.invalid_slug", "error", "slug must be lower-kebab-case"})
}
if !versionPattern.MatchString(manifest.Version) {
findings = append(findings, Finding{"manifest.invalid_version", "error", "version must be semantic versioning"})
}
if !schemaVersions[manifest.SchemaVersion] {
findings = append(findings, Finding{"manifest.invalid_schema_version", "error", "schemaVersion must be 1.0 or attesto.connector.v2"})
}
if !categories[manifest.Category] {
findings = append(findings, Finding{"manifest.invalid_category", "error", "unsupported category"})
}
if len(manifest.Capabilities) == 0 {
findings = append(findings, Finding{"capabilities.empty", "error", "at least one capability is required"})
}
if manifest.SchemaVersion == "" || manifest.Name == "" || manifest.AssetType == "" {
findings = append(findings, Finding{"manifest.missing_field", "error", "schemaVersion, name, and assetType are required"})
}
if manifest.AssetType == "connector" && manifest.SchemaVersion == "attesto.connector.v2" {
findings = appendV2Findings(manifest, findings)
}
score := 0
if len(findings) == 0 {
score = 0
if manifest.Evidence["receipts"] && manifest.Evidence["offlineVerification"] {
score += 25
} else {
score += 18
}
if manifest.Security["secretScan"] && manifest.Security["dependencyScan"] {
score += 20
} else {
score += 12
}
if manifest.Evidence["witnessCompatible"] {
score += 15
} else {
score += 8
}
if manifest.Repository["url"] != "" && manifest.Documentation["url"] != "" {
score += 15
} else {
score += 10
}
if hasCapability(manifest.Capabilities, "proofstream") {
score += 15
} else {
score += 10
}
score += 5
}
tier := "hidden"
switch {
case score >= 95:
tier = "platinum"
case score >= 85:
tier = "gold"
case score >= 70:
tier = "silver"
case score >= 50:
tier = "community"
}
if score < 50 && len(findings) == 0 {
findings = append(findings, Finding{"score.hidden", "error", "Evidence Score below 50 cannot be publicly listed"})
}
return ValidationResult{OK: len(findings) == 0 && score >= 50, EvidenceScore: score, Tier: tier, Findings: findings}
}
func appendV2Findings(manifest Manifest, findings []Finding) []Finding {
if !hasRequiredKeys(manifest.Provider, []string{"id", "name", "websiteUrl"}) {
findings = append(findings, Finding{"provider.invalid", "error", "provider.id, provider.name and provider.websiteUrl are required"})
}
if !hasRequiredKeys(manifest.Auth, []string{"mode", "scopes"}) {
findings = append(findings, Finding{"auth.invalid", "error", "auth.mode and auth.scopes are required"})
} else if !supportedAuthModes[toString(manifest.Auth["mode"])] {
findings = append(findings, Finding{"auth.invalid_mode", "error", "unsupported auth.mode"})
} else if len(stringSet(manifest.Auth["scopes"])) == 0 {
findings = append(findings, Finding{"auth.invalid_scopes", "error", "auth.scopes must be a non-empty list"})
}
if !hasRequiredKeys(manifest.Sync, []string{"modes", "supportsReplay", "rateLimitPolicy"}) {
findings = append(findings, Finding{"sync.invalid", "error", "sync.modes, sync.supportsReplay and sync.rateLimitPolicy are required"})
} else {
modes := stringSet(manifest.Sync["modes"])
if len(modes) == 0 || !subset(modes, supportedSyncModes) {
findings = append(findings, Finding{"sync.invalid_modes", "error", "unsupported sync.modes"})
}
}
if len(manifest.EventTypes) == 0 {
findings = append(findings, Finding{"event_types.invalid", "error", "eventTypes must be non-empty strings"})
}
if !hasRequiredKeys(manifest.SourceTime, []string{"required", "timezonePolicy"}) {
findings = append(findings, Finding{"source_time.invalid", "error", "sourceTime.required and sourceTime.timezonePolicy are required"})
} else if manifest.SourceTime["required"] != true {
findings = append(findings, Finding{"source_time.not_required", "error", "sourceTime.required must be true for production connectors"})
}
if manifest.ConfigSchema["type"] != "object" {
findings = append(findings, Finding{"configSchema.invalid", "error", "configSchema.type must be object"})
}
if manifest.SecretSchema["type"] != "object" {
findings = append(findings, Finding{"secretSchema.invalid", "error", "secretSchema.type must be object"})
}
if !hasRequiredKeys(manifest.Diagnostics, requiredDiagnostics) {
findings = append(findings, Finding{"diagnostics.incomplete", "error", "diagnostics must cover auth, connection, lag, replay and revoke state"})
}
if !hasRequiredKeys(manifest.Runtime, []string{"officialConnectorKit", "sdkSurfaces", "requiredMethods", "canary"}) {
findings = append(findings, Finding{"runtime.incomplete", "error", "runtime must declare connector-kit, SDK surfaces, methods and canary"})
} else {
if manifest.Runtime["officialConnectorKit"] != true {
findings = append(findings, Finding{"runtime.unofficial", "error", "runtime.officialConnectorKit must be true"})
}
surfaces := stringSet(manifest.Runtime["sdkSurfaces"])
for _, surface := range []string{"python", "typescript", "go", "cli"} {
if !surfaces[surface] {
findings = append(findings, Finding{"runtime.sdk_surfaces", "error", "runtime.sdkSurfaces must include python, typescript, go and cli"})
break
}
}
methods := stringSet(manifest.Runtime["requiredMethods"])
for _, method := range requiredRuntimeMethods {
if !methods[method] {
findings = append(findings, Finding{"runtime.required_methods", "error", "runtime.requiredMethods is missing connector runtime methods"})
break
}
}
canary, ok := manifest.Runtime["canary"].(map[string]any)
if !ok || canary["status"] != "green" {
findings = append(findings, Finding{"runtime.canary", "error", "runtime.canary.status must be green before publication"})
}
}
if !hasRequiredKeys(manifest.InstallRequirements, []string{"tenantLoginRequired", "entitlementRequired"}) {
findings = append(findings, Finding{"install_requirements.invalid", "error", "installRequirements must declare tenant login and entitlement requirements"})
} else if manifest.InstallRequirements["tenantLoginRequired"] != true || manifest.InstallRequirements["entitlementRequired"] != true {
findings = append(findings, Finding{"install_requirements.unsafe", "error", "install/download/configure must require tenant login and entitlement"})
}
if len(manifest.Changelog) == 0 {
findings = append(findings, Finding{"changelog.invalid", "error", "changelog must include versioned entries"})
}
return findings
}
func hasRequiredKeys(value map[string]any, keys []string) bool {
if value == nil {
return false
}
for _, key := range keys {
if _, ok := value[key]; !ok {
return false
}
}
return true
}
func toString(value any) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
func stringSet(value any) map[string]bool {
out := map[string]bool{}
switch values := value.(type) {
case []string:
for _, item := range values {
if item != "" {
out[item] = true
}
}
case []any:
for _, item := range values {
if text, ok := item.(string); ok && text != "" {
out[text] = true
}
}
}
return out
}
func subset(values map[string]bool, allowed map[string]bool) bool {
for value := range values {
if !allowed[value] {
return false
}
}
return true
}
func hasCapability(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}