v3/implementation/adrs/ADR-044-ipfs-plugin-registry.md
Proposed
The current plugin discovery system uses a demo mode with hardcoded plugins. When IPNS resolution fails (which it always does since the client is stubbed), it falls back to getDemoPlugins().
We need a production-ready plugin registry that:
Implement a hybrid architecture using:
┌─────────────────────────────────────────────────────────────────┐
│ Publishing Pipeline │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────────┐ ┌─────────┐ │
│ │ GitHub │────▶│ Cloud Build │────▶│ GCS │ │
│ │ Push │ │ (CI/CD) │ │ Bucket │ │
│ └─────────┘ └─────────────┘ └────┬────┘ │
│ │ │
│ ┌─────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Cloud │ │
│ │ Function │ │
│ │ (Publish) │ │
│ └──────┬──────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Pinata │ │ IPNS │ │ Sign │ │
│ │ Pin │ │ Update │ │ Registry │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Discovery Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Claude Flow │────▶│ Resolve │────▶│ Fetch │ │
│ │ CLI │ │ IPNS │ │ IPFS │ │
│ └─────────────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ┌────────┴────────┐ ┌──────┴──────┐ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ dweb.link│ │ ipfs.io │ │ Pinata │ │
│ │ Gateway │ │ Gateway │ │ Gateway │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ Verify Signature │ │
│ │ (Ed25519) │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
# Create bucket for registry data
gcloud storage buckets create gs://claude-flow-plugin-registry \
--location=US \
--uniform-bucket-level-access
# Enable versioning for rollback
gsutil versioning set on gs://claude-flow-plugin-registry
# Service account for Cloud Functions
gcloud iam service-accounts create plugin-registry-publisher \
--display-name="Plugin Registry Publisher"
# Grant permissions
gsutil iam ch serviceAccount:[email protected]:objectViewer gs://claude-flow-plugin-registry
// scripts/setup-pinata.ts
import PinataSDK from '@pinata/sdk';
const pinata = new PinataSDK({
pinataApiKey: process.env.PINATA_API_KEY,
pinataSecretApiKey: process.env.PINATA_SECRET_KEY,
});
// Generate IPNS key for the registry
async function setupIPNS() {
const keyName = 'claude-flow-official-registry';
const key = await pinata.generateKey({
keyName,
permissions: {
endpoints: {
pinning: {
pinFileToIPFS: true,
pinJSONToIPFS: true,
},
},
},
});
console.log('IPNS Key:', key);
// Save this key securely - it's needed for updates
}
// src/plugins/store/registry-schema.ts
import { z } from 'zod';
export const PluginEntrySchema = z.object({
id: z.string(),
name: z.string(),
displayName: z.string(),
description: z.string(),
version: z.string(),
cid: z.string(), // IPFS CID of the plugin tarball
size: z.number(),
checksum: z.string(), // sha256:xxx
author: z.object({
id: z.string(),
displayName: z.string(),
verified: z.boolean(),
}),
license: z.string(),
categories: z.array(z.string()),
tags: z.array(z.string()),
downloads: z.number(),
rating: z.number(),
lastUpdated: z.string().datetime(),
minClaudeFlowVersion: z.string(),
dependencies: z.array(z.object({
name: z.string(),
version: z.string(),
})),
type: z.enum(['core', 'command', 'integration', 'agent', 'theme']),
hooks: z.array(z.string()),
commands: z.array(z.string()),
permissions: z.array(z.string()),
exports: z.array(z.string()),
verified: z.boolean(),
trustLevel: z.enum(['official', 'verified', 'community', 'unverified']),
securityAudit: z.object({
auditor: z.string(),
auditDate: z.string().datetime(),
passed: z.boolean(),
}).optional(),
});
export const PluginRegistrySchema = z.object({
version: z.string(),
type: z.literal('plugins'),
updatedAt: z.string().datetime(),
ipnsName: z.string(),
plugins: z.array(PluginEntrySchema),
categories: z.array(z.object({
id: z.string(),
name: z.string(),
description: z.string(),
pluginCount: z.number(),
})),
totalPlugins: z.number(),
totalDownloads: z.number(),
featured: z.array(z.string()),
trending: z.array(z.string()),
newest: z.array(z.string()),
official: z.array(z.string()),
// Ed25519 signature of the registry content
registrySignature: z.string().optional(),
registryPublicKey: z.string().optional(),
});
export type PluginEntry = z.infer<typeof PluginEntrySchema>;
export type PluginRegistry = z.infer<typeof PluginRegistrySchema>;
// cloud-functions/publish-registry/index.ts
import { Storage } from '@google-cloud/storage';
import PinataSDK from '@pinata/sdk';
import * as ed from '@noble/ed25519';
import { PluginRegistrySchema } from './schema';
const storage = new Storage();
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
});
export async function publishRegistry(req: any, res: any) {
try {
// 1. Fetch registry from GCS
const bucket = storage.bucket('claude-flow-plugin-registry');
const file = bucket.file('registry.json');
const [content] = await file.download();
const registry = JSON.parse(content.toString());
// 2. Validate schema
const validated = PluginRegistrySchema.parse(registry);
// 3. Sign registry
const privateKey = Buffer.from(process.env.REGISTRY_PRIVATE_KEY!, 'hex');
const message = JSON.stringify(validated);
const signature = await ed.signAsync(
new TextEncoder().encode(message),
privateKey
);
validated.registrySignature = Buffer.from(signature).toString('hex');
validated.registryPublicKey = `ed25519:${Buffer.from(
await ed.getPublicKeyAsync(privateKey)
).toString('hex')}`;
// 4. Pin to IPFS via Pinata
const pinResult = await pinata.pinJSONToIPFS(validated, {
pinataMetadata: {
name: 'claude-flow-plugin-registry',
keyvalues: {
version: validated.version,
updatedAt: validated.updatedAt,
},
},
});
console.log(`Pinned to IPFS: ${pinResult.IpfsHash}`);
// 5. Update IPNS pointer
// Note: Pinata's IPNS update requires their Dedicated Gateways plan
// Alternative: Use web3.storage's w3name or run your own IPFS node
res.json({
success: true,
cid: pinResult.IpfsHash,
gateway: `https://gateway.pinata.cloud/ipfs/${pinResult.IpfsHash}`,
publicGateway: `https://ipfs.io/ipfs/${pinResult.IpfsHash}`,
});
} catch (error) {
console.error('Publish failed:', error);
res.status(500).json({ error: String(error) });
}
}
// src/transfer/ipfs/client.ts (updated)
import * as crypto from 'crypto';
const GATEWAYS = [
'https://gateway.pinata.cloud',
'https://cloudflare-ipfs.com',
'https://ipfs.io',
'https://dweb.link',
];
const IPNS_RESOLVERS = [
'https://gateway.pinata.cloud',
'https://dweb.link',
];
/**
* Resolve IPNS name to CID with fallback
*/
export async function resolveIPNS(
ipnsName: string,
preferredGateway?: string
): Promise<string | null> {
const resolvers = preferredGateway
? [preferredGateway, ...IPNS_RESOLVERS]
: IPNS_RESOLVERS;
for (const gateway of resolvers) {
try {
// DNSLink resolution for human-readable names
if (ipnsName.includes('.')) {
const response = await fetch(
`${gateway}/api/v0/name/resolve?arg=/ipns/${ipnsName}`,
{ signal: AbortSignal.timeout(10000) }
);
if (response.ok) {
const data = await response.json();
return data.Path?.replace('/ipfs/', '') || null;
}
}
// Standard IPNS key resolution
const response = await fetch(`${gateway}/ipns/${ipnsName}`, {
method: 'HEAD',
signal: AbortSignal.timeout(10000),
redirect: 'follow',
});
// Extract CID from redirect URL
const finalUrl = response.url;
const cidMatch = finalUrl.match(/\/ipfs\/([a-zA-Z0-9]+)/);
if (cidMatch) {
return cidMatch[1];
}
} catch (error) {
console.warn(`[IPFS] Gateway ${gateway} failed:`, error);
continue;
}
}
return null;
}
/**
* Fetch content from IPFS with fallback gateways
*/
export async function fetchFromIPFS<T>(
cid: string,
preferredGateway?: string
): Promise<T | null> {
const gateways = preferredGateway
? [preferredGateway, ...GATEWAYS]
: GATEWAYS;
for (const gateway of gateways) {
try {
const url = `${gateway}/ipfs/${cid}`;
const response = await fetch(url, {
signal: AbortSignal.timeout(30000),
headers: {
'Accept': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
console.log(`[IPFS] Fetched from ${gateway}`);
return data as T;
}
} catch (error) {
console.warn(`[IPFS] Gateway ${gateway} failed:`, error);
continue;
}
}
return null;
}
/**
* Verify Ed25519 signature
*/
export async function verifySignature(
message: string,
signature: string,
publicKey: string
): Promise<boolean> {
try {
const ed = await import('@noble/ed25519');
const pubKeyHex = publicKey.replace('ed25519:', '');
return await ed.verifyAsync(
Buffer.from(signature, 'hex'),
new TextEncoder().encode(message),
Buffer.from(pubKeyHex, 'hex')
);
} catch {
return false;
}
}
/**
* Check if CID is pinned on a gateway
*/
export async function isPinned(
cid: string,
gateway: string = 'https://ipfs.io'
): Promise<boolean> {
try {
const response = await fetch(`${gateway}/ipfs/${cid}`, {
method: 'HEAD',
signal: AbortSignal.timeout(5000),
});
return response.ok;
} catch {
return false;
}
}
export function getGatewayUrl(cid: string, gateway: string = 'https://ipfs.io'): string {
return `${gateway}/ipfs/${cid}`;
}
export function isValidCID(cid: string): boolean {
return /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|b[a-z2-7]{58,})$/.test(cid);
}
export function hashContent(content: Buffer): string {
return crypto.createHash('sha256').update(content).digest('hex');
}
// Update DEFAULT_PLUGIN_STORE_CONFIG in discovery.ts
export const DEFAULT_PLUGIN_STORE_CONFIG: PluginStoreConfig = {
registries: [
{
name: 'claude-flow-official',
description: 'Official Claude Flow plugin registry',
// Real IPNS name from Pinata
ipnsName: 'k51qzi5uqu5dl...', // Your actual IPNS key
gateway: 'https://gateway.pinata.cloud',
publicKey: 'ed25519:...', // Your public key
trusted: true,
official: true,
},
],
defaultRegistry: 'claude-flow-official',
gateway: 'https://gateway.pinata.cloud',
timeout: 30000,
cacheDir: '.claude-flow/plugins/cache',
cacheExpiry: 3600000,
requireVerification: true,
requireSecurityAudit: false,
minTrustLevel: 'community',
trustedAuthors: ['claude-flow-team'],
blockedPlugins: [],
allowedPermissions: ['network', 'filesystem', 'memory', 'hooks'],
requirePermissionPrompt: true,
};
| Service | Monthly Cost |
|---|---|
| Pinata Free | $0 (1GB pinned, 100 pins) |
| Pinata Picnic | $20 (100GB, 10K pins) |
| GCS (< 1GB) | ~$0.02 |
| Cloud Function | ~$0 (free tier) |
| Total | $0 - $20/month |