feat(D.5): attesto connector init — marketplace-ready scaffold

`attesto connector init <slug> [--name --category --dir]` generates
attesto.connector.json (v2 manifest), webhook_handler.py wired to the
P1.4 verify_webhook helper with its real signature, and a README with the
submission flow; `--validate-only <dir>` re-runs the marketplace
validator (the same connectorkit.ValidateManifest code) as a local
pre-submission check.

Honesty rule: a fresh scaffold cannot claim a green assurance canary, so
runtime.canary ships as "pending" and the validator's single remaining
finding IS the submission to-do list; the scaffold errors if its template
ever drifts into any other finding. Verified end-to-end: generated stub
accepts a genuinely signed webhook and rejects a forged signature against
the published Python SDK; overwrite refusal tested; Go suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-12 11:34:47 +02:00
parent 8dd6c4a784
commit 53ae31e196
3 changed files with 303 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
package main
// [D.5] `attesto connector init <slug>` — scaffold a marketplace-ready
// connector: a v2 manifest that passes connectorkit validation locally, a
// signed-webhook handler stub built on the P1.4 verification helper, and a
// README pointing at the submission flow. The local validation run is the
// same code the marketplace runs, so a green scaffold is a green
// pre-submission check.
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"go.attesto.eu/sdk/connectorkit"
)
var connectorSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,95}$`)
func connectorManifestTemplate(slug, name, category string) connectorkit.Manifest {
return connectorkit.Manifest{
SchemaVersion: "attesto.connector.v2",
Slug: slug,
Name: name,
Version: "0.1.0",
AssetType: "connector",
Category: category,
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"},
Capabilities: []string{
"proofstream", "signed-webhook", "offline-verification",
},
Evidence: map[string]bool{
"offlineVerification": true,
"receipts": true,
"witnessCompatible": true,
},
Security: map[string]bool{
"dependencyScan": true,
"secretScan": true,
"secretsServerSide": true,
},
SupportedLanguages: []string{"en"},
Provider: map[string]any{
"id": slug,
"name": name,
"websiteUrl": "https://example.com",
},
Auth: map[string]any{
"mode": "signed-webhook",
"scopes": []string{"webhook:read"},
},
Sync: map[string]any{
"modes": []string{"webhook"},
"supportsReplay": true,
"rateLimitPolicy": "Provider webhook retries and Attesto idempotency keys",
},
EventTypes: []string{slug + ".event"},
SourceTime: map[string]any{
"required": true,
"timezonePolicy": "source-timestamp-with-offset-required",
},
ConfigSchema: map[string]any{
"type": "object",
"required": []string{"resourceRef"},
"properties": map[string]any{
"resourceRef": map[string]any{"type": "string"},
},
},
SecretSchema: map[string]any{
"type": "object",
"required": []string{"webhookSecret"},
"properties": map[string]any{
"webhookSecret": map[string]any{"type": "string", "secret": true},
},
},
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",
},
// 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",
},
},
InstallRequirements: map[string]any{
"tenantLoginRequired": true,
"entitlementRequired": true,
},
Changelog: []map[string]any{
{"version": "0.1.0", "changes": []string{"Initial scaffold."}},
},
}
}
const webhookHandlerStub = `"""Signed-webhook handler stub 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
skew window and constant-time comparison.
"""
import os
from attesto.webhooks import verify_webhook
def handle(headers: dict[str, str], body: bytes) -> dict:
if not verify_webhook(
body=body,
headers=headers,
secret=os.environ["WEBHOOK_SECRET"],
):
raise PermissionError("invalid webhook signature or stale timestamp")
# The payload is now authentic: turn it into a proofstream event here.
return {"ok": True}
`
const connectorReadmeStub = `# %s
Scaffolded by ` + "`attesto connector init`" + `.
1. Edit ` + "`attesto.connector.json`" + ` (publisher, repository, provider,
event types — search for CHANGE ME).
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:
attesto connector init --validate-only %s
4. Submit through the marketplace flow (docs.attesto.eu/manuals/connectors.html).
`
func (a *app) connectorInit(args []string) error {
fs := flag.NewFlagSet("connector init", flag.ContinueOnError)
fs.SetOutput(a.err)
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>)")
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 := ""
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
slug = args[0]
args = args[1:]
}
if err := fs.Parse(args); err != nil {
return err
}
if *validateOnly != "" {
raw, err := os.ReadFile(filepath.Join(*validateOnly, "attesto.connector.json"))
if err != nil {
return err
}
var manifest connectorkit.Manifest
if err := json.Unmarshal(raw, &manifest); err != nil {
return err
}
result := connectorkit.ValidateManifest(manifest)
if err := a.write(result); err != nil {
return err
}
if !result.OK {
return errors.New("manifest validation failed")
}
return nil
}
if slug == "" && fs.NArg() == 1 {
slug = fs.Arg(0)
}
if slug == "" {
return errors.New("usage: attesto connector init <slug> [--name ...] [--category ...]")
}
if !connectorSlugPattern.MatchString(slug) {
return fmt.Errorf("slug %q must match %s", slug, connectorSlugPattern)
}
connectorName := *name
if connectorName == "" {
connectorName = strings.Title(strings.ReplaceAll(slug, "-", " ")) //nolint:staticcheck
}
manifest := connectorManifestTemplate(slug, connectorName, *category)
result := connectorkit.ValidateManifest(manifest)
// The only acceptable finding on a fresh scaffold is the pending canary —
// everything else must already satisfy the marketplace validator.
for _, finding := range result.Findings {
if finding.Code != "runtime.canary" {
return fmt.Errorf("internal error: scaffold template failed validation: %+v", result.Findings)
}
}
outDir := *dir
if outDir == "" {
outDir = slug
}
if _, err := os.Stat(filepath.Join(outDir, "attesto.connector.json")); err == nil {
return fmt.Errorf("%s/attesto.connector.json already exists", outDir)
}
if err := os.MkdirAll(outDir, 0o755); err != nil {
return err
}
raw, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(outDir, "attesto.connector.json"), append(raw, '\n'), 0o644); err != nil {
return err
}
handler := fmt.Sprintf(webhookHandlerStub, connectorName)
if err := os.WriteFile(filepath.Join(outDir, "webhook_handler.py"), []byte(handler), 0o644); err != nil {
return err
}
readme := fmt.Sprintf(connectorReadmeStub, connectorName, outDir)
if err := os.WriteFile(filepath.Join(outDir, "README.md"), []byte(readme), 0o644); err != nil {
return err
}
return a.write(map[string]any{
"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",
})
}

View File

@@ -0,0 +1,49 @@
package main
// [D.5] connector init scaffolds a manifest whose ONLY validation finding is
// the pending canary, and the generated webhook stub calls the real P1.4
// helper with its actual signature.
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"go.attesto.eu/sdk/connectorkit"
)
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 {
t.Fatal(err)
}
raw, err := os.ReadFile(filepath.Join(dir, "attesto.connector.json"))
if err != nil {
t.Fatal(err)
}
var manifest connectorkit.Manifest
if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatal(err)
}
result := connectorkit.ValidateManifest(manifest)
for _, finding := range result.Findings {
if finding.Code != "runtime.canary" {
t.Fatalf("unexpected finding: %+v", finding)
}
}
stub, err := os.ReadFile(filepath.Join(dir, "webhook_handler.py"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(stub), "from attesto.webhooks import verify_webhook") {
t.Fatal("stub 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 {
t.Fatal("expected overwrite refusal")
}
}

View File

@@ -137,6 +137,12 @@ func (a *app) dispatch(ctx context.Context, args []string) error {
return a.quorum(ctx, args[1:]) return a.quorum(ctx, args[1:])
case "ivc": case "ivc":
return a.ivc(ctx, args[1:]) return a.ivc(ctx, args[1:])
case "connector":
// [D.5] scaffold + local pre-submission validation
if len(args) > 1 && args[1] == "init" {
return a.connectorInit(args[2:])
}
return errors.New("connector subcommand required (init)")
case "connectors": case "connectors":
return a.connectors(ctx, args[1:]) return a.connectors(ctx, args[1:])
case "local-vault": case "local-vault":