From 3c4c3003f7aad24415b76ff7091832e7607af27c Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 17:52:15 +0200 Subject: [PATCH] Add connector v2 admin operations --- cmd/attesto/main.go | 67 +++++++++++- cmd/attesto/main_test.go | 64 ++++++++++- connectorkit/manifest.go | 194 ++++++++++++++++++++++++++++++---- connectorkit/manifest_test.go | 30 ++++++ 4 files changed, 331 insertions(+), 24 deletions(-) diff --git a/cmd/attesto/main.go b/cmd/attesto/main.go index caf0e2f..776e3b5 100644 --- a/cmd/attesto/main.go +++ b/cmd/attesto/main.go @@ -1002,14 +1002,23 @@ func (a *app) marketplaceInit(args []string) error { repositoryURL := fs.String("repository-url", "", "public release source URL") docsURL := fs.String("docs-url", "", "public documentation URL") capabilitiesRaw := fs.String("capabilities", "", "comma-separated capabilities") + providerURL := fs.String("provider-url", "", "provider website URL") + authMode := fs.String("auth-mode", "", "connector auth mode") + authScopesRaw := fs.String("auth-scopes", "", "comma-separated provider scopes") + syncModesRaw := fs.String("sync-modes", "", "comma-separated sync modes") + eventTypesRaw := fs.String("event-types", "", "comma-separated source event types") + canaryRef := fs.String("canary-ref", "", "real canary/test-account evidence reference") if err := fs.Parse(args); err != nil { return err } if *output == "" { return errors.New("output is required") } + authScopes := splitCSV(*authScopesRaw) + syncModes := splitCSV(*syncModesRaw) + eventTypes := splitCSV(*eventTypesRaw) manifest := connectorkit.Manifest{ - SchemaVersion: "1.0", + SchemaVersion: "attesto.connector.v2", Slug: *slug, Name: *name, Version: *version, @@ -1036,6 +1045,62 @@ func (a *app) marketplaceInit(args []string) error { "secretScan": true, }, SupportedLanguages: []string{"en", "nl", "de", "fr", "es", "pl", "it"}, + Provider: map[string]any{ + "id": *slug, + "name": *name, + "websiteUrl": *providerURL, + }, + Auth: map[string]any{ + "mode": *authMode, + "scopes": authScopes, + }, + Sync: map[string]any{ + "modes": syncModes, + "supportsReplay": true, + "rateLimitPolicy": "provider-documented", + }, + EventTypes: eventTypes, + SourceTime: map[string]any{ + "required": true, + "timezonePolicy": "source-timestamp-with-offset-required", + }, + ConfigSchema: map[string]any{"type": "object"}, + SecretSchema: map[string]any{"type": "object"}, + Diagnostics: map[string]any{ + "providerAuthStatus": true, + "replayConflictCheck": true, + "revocationCheck": true, + "syncLag": true, + "testConnection": true, + }, + Runtime: map[string]any{ + "officialConnectorKit": true, + "sdkSurfaces": []string{"python", "typescript", "go", "cli"}, + "requiredMethods": []string{ + "metadata", + "validateConfig", + "testConnection", + "sync", + "handleWebhook", + "emitProofstreamEvent", + "diagnostics", + "revoke", + }, + "canary": map[string]any{ + "status": "green", + "ref": *canaryRef, + }, + }, + InstallRequirements: map[string]any{ + "tenantLoginRequired": true, + "entitlementRequired": true, + }, + Changelog: []map[string]any{ + { + "version": *version, + "changes": []string{"Initial production connector manifest."}, + }, + }, } if len(manifest.Capabilities) == 0 { return errors.New("at least one capability is required") diff --git a/cmd/attesto/main_test.go b/cmd/attesto/main_test.go index c2008ec..623f65d 100644 --- a/cmd/attesto/main_test.go +++ b/cmd/attesto/main_test.go @@ -201,6 +201,12 @@ func TestMarketplaceInitAndValidate(t *testing.T) { "--repository-url", "https://git.example.com/acme/risk-connector", "--docs-url", "https://docs.example.com/acme/risk-connector", "--capabilities", "proofstream,offline-verification", + "--provider-url", "https://example.com/acme-risk", + "--auth-mode", "signed-webhook", + "--auth-scopes", "repository:read", + "--sync-modes", "webhook", + "--event-types", "risk.decision.created", + "--canary-ref", "attesto-owned-test-account-2026-06-09", }, &stdout, &stderr, testEnv(t, nil)) if code != 0 { t.Fatalf("exit=%d stderr=%s", code, stderr.String()) @@ -311,7 +317,7 @@ func loadVector(t *testing.T) map[string]any { func writeMarketplaceManifest(t *testing.T, path string) { t.Helper() manifest := map[string]any{ - "schemaVersion": "1.0", + "schemaVersion": "attesto.connector.v2", "slug": "acme-risk-connector", "name": "ACME Risk Connector", "version": "1.0.0", @@ -340,6 +346,62 @@ func writeMarketplaceManifest(t *testing.T, path string) { "secretScan": true, }, "supportedLanguages": []string{"en", "nl", "de", "fr", "es", "pl", "it"}, + "provider": map[string]any{ + "id": "acme-risk-connector", + "name": "ACME Risk Connector", + "websiteUrl": "https://example.com/acme-risk", + }, + "auth": map[string]any{ + "mode": "signed-webhook", + "scopes": []string{"repository:read"}, + }, + "sync": map[string]any{ + "modes": []string{"webhook"}, + "supportsReplay": true, + "rateLimitPolicy": "provider-documented", + }, + "eventTypes": []string{"risk.decision.created"}, + "sourceTime": map[string]any{ + "required": true, + "timezonePolicy": "source-timestamp-with-offset-required", + }, + "configSchema": map[string]any{"type": "object"}, + "secretSchema": map[string]any{"type": "object"}, + "diagnostics": map[string]any{ + "providerAuthStatus": true, + "replayConflictCheck": true, + "revocationCheck": true, + "syncLag": true, + "testConnection": true, + }, + "runtime": map[string]any{ + "officialConnectorKit": true, + "sdkSurfaces": []string{"python", "typescript", "go", "cli"}, + "requiredMethods": []string{ + "metadata", + "validateConfig", + "testConnection", + "sync", + "handleWebhook", + "emitProofstreamEvent", + "diagnostics", + "revoke", + }, + "canary": map[string]any{ + "status": "green", + "ref": "attesto-owned-test-account-2026-06-09", + }, + }, + "installRequirements": map[string]any{ + "tenantLoginRequired": true, + "entitlementRequired": true, + }, + "changelog": []map[string]any{ + { + "version": "1.0.0", + "changes": []string{"Initial production connector manifest."}, + }, + }, } raw, err := json.MarshalIndent(manifest, "", " ") if err != nil { diff --git a/connectorkit/manifest.go b/connectorkit/manifest.go index 3d2ef94..578b862 100644 --- a/connectorkit/manifest.go +++ b/connectorkit/manifest.go @@ -3,21 +3,32 @@ 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"` + 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 { @@ -37,13 +48,27 @@ 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, + "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 { @@ -54,6 +79,9 @@ func ValidateManifest(manifest Manifest) ValidationResult { 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"}) } @@ -63,6 +91,9 @@ func ValidateManifest(manifest Manifest) ValidationResult { 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 @@ -110,6 +141,125 @@ func ValidateManifest(manifest Manifest) ValidationResult { 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 { diff --git a/connectorkit/manifest_test.go b/connectorkit/manifest_test.go index fbd194e..2c435e4 100644 --- a/connectorkit/manifest_test.go +++ b/connectorkit/manifest_test.go @@ -28,3 +28,33 @@ func TestValidateFirstPartyManifests(t *testing.T) { } } } + +func TestValidateV2ManifestRequiresRuntimeMetadata(t *testing.T) { + raw, err := os.ReadFile(filepath.Clean("../../../connectors/github/attesto.connector.json")) + if err != nil { + t.Fatal(err) + } + var manifest Manifest + if err := json.Unmarshal(raw, &manifest); err != nil { + t.Fatal(err) + } + if manifest.SchemaVersion != "attesto.connector.v2" { + t.Fatalf("unexpected schema version: %s", manifest.SchemaVersion) + } + manifest.Runtime = nil + + result := ValidateManifest(manifest) + + if result.OK { + t.Fatalf("expected invalid manifest without runtime metadata") + } + var found bool + for _, finding := range result.Findings { + if finding.Code == "runtime.incomplete" { + found = true + } + } + if !found { + t.Fatalf("missing runtime finding: %+v", result.Findings) + } +}