fix: harden connector manifest validation

This commit is contained in:
Codex
2026-06-16 23:40:33 +02:00
parent 4a4b86ae10
commit 8d6d9bf9c2
5 changed files with 289 additions and 28 deletions
+43 -17
View File
@@ -22,7 +22,13 @@ import (
var connectorSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,95}$`)
func connectorManifestTemplate(slug, name, category string) connectorkit.Manifest {
func connectorManifestTemplate(
slug, name, category, publisherSlug, publisherName, repositoryURL, docsURL, providerURL, canaryRef string,
) connectorkit.Manifest {
canaryStatus := "pending"
if strings.TrimSpace(canaryRef) != "" {
canaryStatus = "green"
}
return connectorkit.Manifest{
SchemaVersion: "attesto.connector.v2",
Slug: slug,
@@ -33,9 +39,9 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
Summary: fmt.Sprintf("Verify %s evidence into Attesto Proofstream.", name),
Description: fmt.Sprintf(
"Produces verifiable evidence for %s events through Attesto Proofstream.", name),
Publisher: map[string]string{"name": "CHANGE ME", "slug": "change-me"},
Repository: map[string]string{"url": "https://example.com/CHANGE-ME/" + slug},
Documentation: map[string]string{"url": "https://docs.attesto.eu/manuals/connectors.html"},
Publisher: map[string]string{"name": publisherName, "slug": publisherSlug},
Repository: map[string]string{"url": repositoryURL},
Documentation: map[string]string{"url": docsURL},
Capabilities: []string{
"proofstream", "signed-webhook", "offline-verification",
},
@@ -53,7 +59,7 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
Provider: map[string]any{
"id": slug,
"name": name,
"websiteUrl": "https://example.com",
"websiteUrl": providerURL,
},
Auth: map[string]any{
"mode": "signed-webhook",
@@ -97,12 +103,9 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
"metadata", "validateConfig", "testConnection", "sync",
"handleWebhook", "emitProofstreamEvent", "diagnostics", "revoke",
},
// A scaffold cannot honestly claim a green assurance canary; this
// stays "pending" (the one expected validation finding) until the
// connector has real canary evidence.
"canary": map[string]any{
"status": "pending",
"ref": "CHANGE ME: assurance canary evidence ref",
"status": canaryStatus,
"ref": canaryRef,
},
},
InstallRequirements: map[string]any{
@@ -115,7 +118,7 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
}
}
const webhookHandlerTemplate = `"""Signed-webhook handler starter for the %s connector.
const webhookHandlerTemplate = `"""Signed-webhook verification helper for the %s connector.
Verification uses the Attesto SDK's P1.4 helper — the same scheme the
platform signs with: HMAC-SHA256 over "{timestamp}.{body}" with a 300s
@@ -134,8 +137,7 @@ def handle(headers: dict[str, str], body: bytes) -> dict:
):
raise PermissionError("invalid webhook signature or stale timestamp")
# The payload is now authentic: turn it into a proofstream event here.
return {"ok": True}
return {"ok": True, "authenticatedPayloadBytes": len(body)}
`
const connectorReadmeTemplate = `# %s
@@ -143,7 +145,7 @@ const connectorReadmeTemplate = `# %s
Scaffolded by ` + "`attesto connector init`" + `.
1. Edit ` + "`attesto.connector.json`" + ` (publisher, repository, provider,
event types — search for CHANGE ME).
event types and canary evidence when applicable).
2. Implement the runtime methods (see ` + "`webhook_handler.py`" + ` for the
signed-webhook entry point; verification is already wired).
3. Re-run the pre-submission check at any time:
@@ -159,6 +161,12 @@ func (a *app) connectorInit(args []string) error {
name := fs.String("name", "", "human-readable connector name (default: derived from slug)")
category := fs.String("category", "devops", "marketplace category")
dir := fs.String("dir", "", "output directory (default: ./<slug>)")
publisherSlug := fs.String("publisher-slug", "", "publisher slug")
publisherName := fs.String("publisher-name", "", "publisher display name")
repositoryURL := fs.String("repository-url", "", "HTTPS repository URL for this connector")
docsURL := fs.String("docs-url", "https://docs.attesto.eu/manuals/connectors.html", "HTTPS documentation URL")
providerURL := fs.String("provider-url", "", "HTTPS provider/product URL")
canaryRef := fs.String("canary-ref", "", "optional real canary/release evidence reference; required before publication")
validateOnly := fs.String("validate-only", "", "validate an existing <dir>/attesto.connector.json and exit")
// Accept the slug positionally before flags: `connector init my-slug --category crm`.
slug := ""
@@ -198,12 +206,31 @@ func (a *app) connectorInit(args []string) error {
if !connectorSlugPattern.MatchString(slug) {
return fmt.Errorf("slug %q must match %s", slug, connectorSlugPattern)
}
if strings.TrimSpace(*publisherSlug) == "" || strings.TrimSpace(*publisherName) == "" {
return errors.New("--publisher-slug and --publisher-name are required; connector init never writes placeholder publisher metadata")
}
if strings.TrimSpace(*repositoryURL) == "" || strings.TrimSpace(*providerURL) == "" {
return errors.New("--repository-url and --provider-url are required; connector init never writes placeholder URLs")
}
if strings.TrimSpace(*docsURL) == "" {
return errors.New("--docs-url is required")
}
connectorName := *name
if connectorName == "" {
connectorName = strings.Title(strings.ReplaceAll(slug, "-", " ")) //nolint:staticcheck
}
manifest := connectorManifestTemplate(slug, connectorName, *category)
manifest := connectorManifestTemplate(
slug,
connectorName,
*category,
*publisherSlug,
*publisherName,
*repositoryURL,
*docsURL,
*providerURL,
*canaryRef,
)
result := connectorkit.ValidateManifest(manifest)
// The only acceptable finding on a fresh scaffold is the pending canary —
// everything else must already satisfy the marketplace validator.
@@ -242,7 +269,6 @@ func (a *app) connectorInit(args []string) error {
"created": outDir,
"files": []string{"attesto.connector.json", "webhook_handler.py", "README.md"},
"validation": result,
"nextSteps": "edit CHANGE ME fields, implement runtime methods, earn a green " +
"assurance canary, re-run with --validate-only until OK",
"nextSteps": "implement runtime methods, attach real canary evidence, re-run with --validate-only until OK",
})
}
+34 -2
View File
@@ -18,13 +18,26 @@ import (
func TestConnectorInitScaffoldsValidManifest(t *testing.T) {
dir := filepath.Join(t.TempDir(), "my-crm")
a := &app{out: &bytes.Buffer{}, err: &bytes.Buffer{}}
if err := a.connectorInit([]string{"my-crm-evidence", "--category", "crm", "--dir", dir}); err != nil {
if err := a.connectorInit([]string{
"my-crm-evidence",
"--category", "crm",
"--dir", dir,
"--publisher-slug", "attesto",
"--publisher-name", "Attesto",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/my-crm-evidence",
"--provider-url", "https://attesto.eu",
}); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(filepath.Join(dir, "attesto.connector.json"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(strings.ToLower(string(raw)), "change me") ||
strings.Contains(strings.ToLower(string(raw)), "change-me") ||
strings.Contains(strings.ToLower(string(raw)), "example.com") {
t.Fatalf("scaffold contains placeholder metadata: %s", string(raw))
}
var manifest connectorkit.Manifest
if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatal(err)
@@ -43,7 +56,26 @@ func TestConnectorInitScaffoldsValidManifest(t *testing.T) {
t.Fatal("generated handler does not use the P1.4 helper")
}
// re-running must refuse to overwrite
if err := a.connectorInit([]string{"my-crm-evidence", "--dir", dir}); err == nil {
if err := a.connectorInit([]string{
"my-crm-evidence",
"--dir", dir,
"--publisher-slug", "attesto",
"--publisher-name", "Attesto",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/my-crm-evidence",
"--provider-url", "https://attesto.eu",
}); err == nil {
t.Fatal("expected overwrite refusal")
}
}
func TestConnectorInitRejectsMissingRealMetadata(t *testing.T) {
dir := filepath.Join(t.TempDir(), "my-crm")
a := &app{out: &bytes.Buffer{}, err: &bytes.Buffer{}}
err := a.connectorInit([]string{"my-crm-evidence", "--category", "crm", "--dir", dir})
if err == nil {
t.Fatal("expected connector init to require real metadata")
}
if _, statErr := os.Stat(filepath.Join(dir, "attesto.connector.json")); !os.IsNotExist(statErr) {
t.Fatalf("connector init wrote files after missing metadata error: %v", statErr)
}
}
+85 -8
View File
@@ -198,10 +198,10 @@ func TestMarketplaceInitAndValidate(t *testing.T) {
"--description", "Produces verifiable Proofstream events for ACME risk decisions.",
"--publisher-slug", "acme",
"--publisher-name", "ACME",
"--repository-url", "https://git.example.com/acme/risk-connector",
"--docs-url", "https://docs.example.com/acme/risk-connector",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/acme-risk-connector",
"--docs-url", "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
"--capabilities", "proofstream,offline-verification",
"--provider-url", "https://example.com/acme-risk",
"--provider-url", "https://attesto.eu/connectors/acme-risk",
"--auth-mode", "signed-webhook",
"--auth-scopes", "repository:read",
"--sync-modes", "webhook",
@@ -226,6 +226,83 @@ func TestMarketplaceInitAndValidate(t *testing.T) {
}
}
func TestMarketplaceInitRejectsMissingCanaryEvidenceRef(t *testing.T) {
dir := t.TempDir()
manifestFile := filepath.Join(dir, "attesto.connector.json")
var stdout, stderr bytes.Buffer
code := run([]string{
"--json",
"marketplace",
"init",
"--output", manifestFile,
"--slug", "acme-risk-connector",
"--name", "ACME Risk Connector",
"--version", "1.0.0",
"--category", "ai-governance",
"--summary", "Produces Attesto evidence for ACME risk decisions.",
"--description", "Produces verifiable Proofstream events for ACME risk decisions.",
"--publisher-slug", "acme",
"--publisher-name", "ACME",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/acme-risk-connector",
"--docs-url", "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
"--capabilities", "proofstream,offline-verification",
"--provider-url", "https://attesto.eu/connectors/acme-risk",
"--auth-mode", "signed-webhook",
"--auth-scopes", "repository:read",
"--sync-modes", "webhook",
"--event-types", "risk.decision.created",
}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("init exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "runtime.canary_ref") {
t.Fatalf("expected missing canary evidence finding: %s", stdout.String())
}
if _, err := os.Stat(manifestFile); !os.IsNotExist(err) {
t.Fatalf("marketplace init wrote a manifest without canary evidence: %v", err)
}
}
func TestMarketplaceInitRejectsPlaceholderMetadata(t *testing.T) {
dir := t.TempDir()
manifestFile := filepath.Join(dir, "attesto.connector.json")
var stdout, stderr bytes.Buffer
code := run([]string{
"--json",
"marketplace",
"init",
"--output", manifestFile,
"--slug", "acme-risk-connector",
"--name", "ACME Risk Connector",
"--version", "1.0.0",
"--category", "ai-governance",
"--summary", "Produces Attesto evidence for ACME risk decisions.",
"--description", "Produces verifiable Proofstream events for ACME risk decisions.",
"--publisher-slug", "acme",
"--publisher-name", "ACME",
"--repository-url", "https://example.com/acme/risk-connector",
"--docs-url", "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
"--capabilities", "proofstream,offline-verification",
"--provider-url", "https://attesto.eu/connectors/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("init exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "manifest.placeholder") {
t.Fatalf("expected placeholder metadata finding: %s", stdout.String())
}
if _, err := os.Stat(manifestFile); !os.IsNotExist(err) {
t.Fatalf("marketplace init wrote a manifest with placeholder metadata: %v", err)
}
}
func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) {
dir := t.TempDir()
manifestFile := filepath.Join(dir, "attesto.connector.json")
@@ -245,7 +322,7 @@ func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("submit json: %v", err)
}
if body["sourceRef"] != "https://git.example.com/acme/risk-connector/releases/v1.0.0" {
if body["sourceRef"] != "https://git.rotz.ai/rotzmediagroup/acme-risk-connector/releases/v1.0.0" {
t.Fatalf("unexpected sourceRef: %#v", body["sourceRef"])
}
_, _ = w.Write([]byte(`{"asset":{"slug":"acme-risk-connector","name":"ACME Risk Connector"},"validation":{"ok":true},"evidence":{"action":"asset_validation_finished","receiptHash":"rh","payloadHash":"ph"}}`))
@@ -271,7 +348,7 @@ func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) {
"--json", "--base-url", server.URL, "--token-env", "ATT_TOKEN",
"marketplace", "submit",
"--manifest-file", manifestFile,
"--source-ref", "https://git.example.com/acme/risk-connector/releases/v1.0.0",
"--source-ref", "https://git.rotz.ai/rotzmediagroup/acme-risk-connector/releases/v1.0.0",
"--visibility", "public",
"--pricing-model", "free",
}, &stdout, &stderr, env)
@@ -330,10 +407,10 @@ func writeMarketplaceManifest(t *testing.T, path string) {
"name": "ACME",
},
"repository": map[string]any{
"url": "https://git.example.com/acme/risk-connector",
"url": "https://git.rotz.ai/rotzmediagroup/acme-risk-connector",
},
"documentation": map[string]any{
"url": "https://docs.example.com/acme/risk-connector",
"url": "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
},
"capabilities": []string{"proofstream", "offline-verification"},
"evidence": map[string]bool{
@@ -349,7 +426,7 @@ func writeMarketplaceManifest(t *testing.T, path string) {
"provider": map[string]any{
"id": "acme-risk-connector",
"name": "ACME Risk Connector",
"websiteUrl": "https://example.com/acme-risk",
"websiteUrl": "https://attesto.eu/connectors/acme-risk",
},
"auth": map[string]any{
"mode": "signed-webhook",