UNPKG

@silvana-one/coordination

Version:

Silvana Coordination Client

865 lines (766 loc) 23.3 kB
import { Transaction } from "@mysten/sui/transactions"; import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui/utils"; import { fetchSuiDynamicField, fetchSuiDynamicFieldsList } from "./fetch.js"; import { silvanaRegistryPackage } from "./package.js"; import { AppMethod } from "./app_instance.js"; type AgentChain = | "ethereum:mainnet" | "ethereum:seplolia" | "ethereum:holesky" | "ethereum:hoodi" | "mina:mainnet" | "mina:devnet" | "zeko:testnet" | "zeko:alphanet" | "sui:mainnet" | "sui:testnet" | "sui:devnet" | "solana:mainnet" | "solana:testnet" | "solana:devnet" | "walrus:mainnet" | "walrus:testnet" | string; // other chains export interface AgentMethod { dockerImage: string; dockerSha256?: string; minMemoryGb: number; minCpuCores: number; requiresTee: boolean; } export interface Agent { id: string; name: string; image?: string; description?: string; site?: string; chains: AgentChain[]; methods: Record<string, AgentMethod>; defaultMethod?: AgentMethod; createdAt: number; updatedAt: number; version: number; } export interface Developer { id: string; name: string; github: string; image?: string; description?: string; site?: string; owner: string; agents: string[]; createdAt: number; updatedAt: number; version: number; } export interface DeveloperNames { id: string; developer_address: string; names: string[]; version: number; } export interface SilvanaApp { id: string; name: string; description?: string; methods: Record<string, AppMethod>; owner: string; createdAt: number; updatedAt: number; version: number; instances: string[]; } export class AgentRegistry { private readonly registry: string; constructor(params: { registry: string }) { this.registry = params.registry; } static createAgentRegistry(params: { name: string; transaction?: Transaction; }): Transaction { const { name, transaction } = params; console.log("Creating agent registry", name); const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::create_registry`, arguments: [tx.pure.string(name)], }); return tx; } createDeveloper(params: { name: string; developerOwner: string; github: string; image?: string; description?: string; site?: string; transaction?: Transaction; }): Transaction { const { name, developerOwner, github, image, description, site, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::add_developer`, arguments: [ tx.object(this.registry), tx.pure.address(developerOwner), tx.pure.string(name), tx.pure.string(github), tx.pure.option("string", image ?? null), tx.pure.option("string", description ?? null), tx.pure.option("string", site ?? null), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } updateDeveloper(params: { name: string; github: string; image?: string; description?: string; site?: string; transaction?: Transaction; }): Transaction { const { name, github, image, description, site, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::update_developer`, arguments: [ tx.object(this.registry), tx.pure.string(name), tx.pure.string(github), tx.pure.option("string", image ?? null), tx.pure.option("string", description ?? null), tx.pure.option("string", site ?? null), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } removeDeveloper(params: { name: string; agentNames: string[]; transaction?: Transaction; }): Transaction { const { name, agentNames, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::remove_developer`, arguments: [ tx.object(this.registry), tx.pure.string(name), tx.pure.vector("string", agentNames), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } createAgent(params: { developer: string; name: string; image?: string; description?: string; site?: string; chains: AgentChain[]; transaction?: Transaction; }): Transaction { const { developer, name, image, description, site, chains, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::add_agent`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(name), tx.pure.option("string", image ?? null), tx.pure.option("string", description ?? null), tx.pure.option("string", site ?? null), tx.pure.vector("string", chains), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } updateAgent(params: { developer: string; name: string; image?: string; description?: string; site?: string; chains: AgentChain[]; transaction?: Transaction; }): Transaction { const { developer, name, image, description, site, chains, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::update_agent`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(name), tx.pure.option("string", image ?? null), tx.pure.option("string", description ?? null), tx.pure.option("string", site ?? null), tx.pure.vector("string", chains), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } removeAgent(params: { developer: string; agent: string; transaction?: Transaction; }): Transaction { const { developer, agent, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::remove_agent`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(agent), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } addAgentMethod(params: { developer: string; agent: string; method: string; dockerImage: string; dockerSha256?: string; minMemoryGb: number; minCpuCores: number; requiresTee: boolean; transaction?: Transaction; }): Transaction { const { developer, agent, method, dockerImage, dockerSha256, minMemoryGb, minCpuCores, requiresTee, transaction, } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::add_method`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(agent), tx.pure.string(method), tx.pure.string(dockerImage), tx.pure.option("string", dockerSha256 ?? null), tx.pure.u16(minMemoryGb), tx.pure.u16(minCpuCores), tx.pure.bool(requiresTee), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } updateAgentMethod(params: { developer: string; agent: string; method: string; dockerImage: string; dockerSha256?: string; minMemoryGb: number; minCpuCores: number; requiresTee: boolean; transaction?: Transaction; }): Transaction { const { developer, agent, method, dockerImage, dockerSha256, minMemoryGb, minCpuCores, requiresTee, transaction, } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::update_method`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(agent), tx.pure.string(method), tx.pure.string(dockerImage), tx.pure.option("string", dockerSha256 ?? null), tx.pure.u16(minMemoryGb), tx.pure.u16(minCpuCores), tx.pure.bool(requiresTee), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } removeAgentMethod(params: { developer: string; agent: string; method: string; transaction?: Transaction; }): Transaction { const { developer, agent, method, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::remove_method`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(agent), tx.pure.string(method), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } addMethodToApp(params: { appName: string; methodName: string; description?: string; developerName: string; agentName: string; agentMethod: string; transaction?: Transaction; }): Transaction { const { appName, methodName, description, developerName, agentName, agentMethod, transaction, } = params; const tx = transaction ?? new Transaction(); // Create the app method using app_method::new const appMethod = tx.moveCall({ target: `${silvanaRegistryPackage}::app_method::new`, arguments: [ tx.pure.option("string", description ?? null), tx.pure.string(developerName), tx.pure.string(agentName), tx.pure.string(agentMethod), ], }); // Add the method to the app tx.moveCall({ target: `${silvanaRegistryPackage}::registry::add_method_to_app`, arguments: [ tx.object(this.registry), tx.pure.string(appName), tx.pure.string(methodName), appMethod, ], }); return tx; } addMetadata(params: { appInstanceId: string; key: string; value: string; transaction?: Transaction; }): Transaction { const { appInstanceId, key, value, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::app_instance::add_metadata`, arguments: [ tx.object(appInstanceId), tx.pure.string(key), tx.pure.string(value), ], }); return tx; } setDefaultMethod(params: { developer: string; agent: string; method: string; transaction?: Transaction; }): Transaction { const { developer, agent, method, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::set_default_method`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(agent), tx.pure.string(method), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } removeDefaultMethod(params: { developer: string; agent: string; transaction?: Transaction; }): Transaction { const { developer, agent, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::remove_default_method`, arguments: [ tx.object(this.registry), tx.pure.string(developer), tx.pure.string(agent), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } async getDeveloper(params: { name: string }): Promise<Developer | undefined> { const developerObject = await fetchSuiDynamicField({ objectID: this.registry, fieldName: "developers", type: "0x1::string::String", key: params.name, }); if (!developerObject) { return undefined; } let agents: string[] = []; const agentsObject = (developerObject as any)?.agents?.fields?.id?.id; if (agentsObject) { const agentsList = await fetchSuiDynamicFieldsList(agentsObject); const agentsArray = agentsList?.data as any; if (Array.isArray(agentsArray)) { agents = agentsArray .map((agent: any) => agent?.name?.value) .filter( (agent: any) => agent !== undefined && typeof agent === "string" ); } } const developer = { id: (developerObject as any)?.id?.id, name: (developerObject as any).name, github: (developerObject as any).github, image: (developerObject as any)?.image ?? undefined, description: (developerObject as any)?.description ?? undefined, site: (developerObject as any)?.site ?? undefined, owner: (developerObject as any).owner, agents, createdAt: Number((developerObject as any).created_at), updatedAt: Number((developerObject as any).updated_at), version: Number((developerObject as any).version), }; if ( !developer.id || !developer.name || !developer.github || !developer.owner || !developer.createdAt || !developer.updatedAt ) { return undefined; } return developer as Developer; } async getDeveloperNames(params: { developerAddress: string; }): Promise<DeveloperNames | undefined> { const developerObject = await fetchSuiDynamicField({ objectID: this.registry, fieldName: "developers_index", type: "address", key: params.developerAddress, }); if (!developerObject) { return undefined; } const developer = { id: (developerObject as any)?.id?.id, developer_address: (developerObject as any).developer, names: (developerObject as any).names, version: Number((developerObject as any).version), }; if (!developer.id || !developer.developer_address || !developer.names) { return undefined; } return developer as DeveloperNames; } async getAgent(params: { developer: string; agent: string; }): Promise<Agent | undefined> { const developerObject = await fetchSuiDynamicField({ objectID: this.registry, fieldName: "developers", type: "0x1::string::String", key: params.developer, }); const id = (developerObject as any)?.agents?.fields?.id?.id; if (!id) { return undefined; } const agentObject = await fetchSuiDynamicField({ parentID: id, fieldName: "agents", type: "0x1::string::String", key: params.agent, }); if (!agentObject) { return undefined; } // Parse methods from VecMap structure const methods: Record<string, AgentMethod> = {}; const methodsData = (agentObject as any)?.methods?.fields?.contents; if (methodsData && Array.isArray(methodsData)) { for (const entry of methodsData) { const key = entry?.fields?.key; const value = entry?.fields?.value; if (key && value) { methods[key] = { dockerImage: value.docker_image, dockerSha256: value.docker_sha256 ?? undefined, minMemoryGb: Number(value.min_memory_gb), minCpuCores: Number(value.min_cpu_cores), requiresTee: Boolean(value.requires_tee), }; } } } // Parse default method if it exists let defaultMethod: AgentMethod | undefined; const defaultMethodData = (agentObject as any)?.default_method; if ( defaultMethodData && typeof defaultMethodData === "object" && !Array.isArray(defaultMethodData) ) { defaultMethod = { dockerImage: defaultMethodData.docker_image, dockerSha256: defaultMethodData.docker_sha256 ?? undefined, minMemoryGb: Number(defaultMethodData.min_memory_gb), minCpuCores: Number(defaultMethodData.min_cpu_cores), requiresTee: Boolean(defaultMethodData.requires_tee), }; } const agent = { id: (agentObject as any)?.id?.id, name: (agentObject as any).name, image: (agentObject as any)?.image ?? undefined, description: (agentObject as any)?.description ?? undefined, site: (agentObject as any)?.site ?? undefined, chains: (agentObject as any)?.chains ?? [], methods, defaultMethod, createdAt: Number((agentObject as any).created_at), updatedAt: Number((agentObject as any).updated_at), version: Number((agentObject as any).version), }; // Only check for essential fields if (!agent.id || !agent.name) { return undefined; } return agent as Agent; } async getApp(params: { name: string }): Promise<SilvanaApp | undefined> { const appObject = await fetchSuiDynamicField({ objectID: this.registry, fieldName: "apps", type: "0x1::string::String", key: params.name, }); if (!appObject) { return undefined; } // Parse methods from VecMap structure const methods: Record<string, AppMethod> = {}; const methodsData = (appObject as any)?.methods?.fields?.contents; if (methodsData && Array.isArray(methodsData)) { for (const entry of methodsData) { const key = entry?.fields?.key; const value = entry?.fields?.value?.fields; if (key && value) { methods[key] = { description: value.description ?? undefined, developer: value.developer, agent: value.agent, agentMethod: value.agent_method, }; } } } // Parse instances from VecSet structure const instances: string[] = []; const instancesData = (appObject as any)?.instances?.fields?.contents; if (instancesData && Array.isArray(instancesData)) { for (const instance of instancesData) { if (instance?.fields?.key) { instances.push(instance.fields.key); } } } const app = { id: (appObject as any)?.id?.id, name: (appObject as any).name, description: (appObject as any)?.description ?? undefined, methods, owner: (appObject as any).owner, createdAt: Number((appObject as any).created_at), updatedAt: Number((appObject as any).updated_at), version: Number((appObject as any).version), instances, }; // Check for essential fields if (!app.id || !app.name || !app.owner) { return undefined; } return app as SilvanaApp; } createApp(params: { name: string; owner: string; description?: string; transaction?: Transaction; }): Transaction { const { name, owner, description, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::add_app`, arguments: [ tx.object(this.registry), tx.pure.string(name), tx.pure.address(owner), tx.pure.option("string", description ?? null), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } removeApp(params: { name: string; transaction?: Transaction }): Transaction { const { name, transaction } = params; const tx = transaction ?? new Transaction(); tx.moveCall({ target: `${silvanaRegistryPackage}::registry::remove_app`, arguments: [ tx.object(this.registry), tx.pure.string(name), tx.object(SUI_CLOCK_OBJECT_ID), ], }); return tx; } static async getDockerImageDetails(params: { dockerImage: string }): Promise< | { sha256: string; numberOfLayers: number; } | undefined > { try { const { dockerImage } = params; // Parse image_source to extract repository and tag const colonPos = dockerImage.lastIndexOf(":"); const repository = colonPos !== -1 ? dockerImage.slice(0, colonPos) : dockerImage; const tag = colonPos !== -1 ? dockerImage.slice(colonPos + 1) : "latest"; // 1. Get token const tokenResponse = await fetch( "https://auth.docker.io/token?" + new URLSearchParams({ service: "registry.docker.io", scope: `repository:${repository}:pull`, }) ); if (!tokenResponse.ok) { return undefined; } const tokenData = await tokenResponse.json(); const token = tokenData.token; if (!token) { return undefined; } // 2. Fetch manifest/index const manifestResponse = await fetch( `https://registry-1.docker.io/v2/${repository}/manifests/${tag}`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.v2+json", }, } ); if (!manifestResponse.ok) { return undefined; } const contentType = manifestResponse.headers.get("content-type") || ""; // Extract the digest from the response headers let digest = manifestResponse.headers.get("docker-content-digest") || ""; let manifest: any; if (contentType.includes("index") || contentType.includes("list")) { // This is a manifest index (multi-platform) const idx = await manifestResponse.json(); // Pick amd64/linux manifest const platformManifest = idx.manifests?.find( (m: any) => m.platform?.architecture === "amd64" && m.platform?.os === "linux" ); if (!platformManifest) { return undefined; } const platformDigest = platformManifest.digest; // 3. Fetch the actual manifest const actualManifestResponse = await fetch( `https://registry-1.docker.io/v2/${repository}/manifests/${platformDigest}`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.docker.distribution.manifest.v2+json", }, } ); if (!actualManifestResponse.ok) { return undefined; } manifest = await actualManifestResponse.json(); // Update digest from the actual manifest response const actualDigest = actualManifestResponse.headers.get( "docker-content-digest" ); if (actualDigest) { digest = actualDigest; } } else { // This is already a direct manifest (single platform) manifest = await manifestResponse.json(); } if (!manifest?.layers || !Array.isArray(manifest.layers)) { return undefined; } const numberOfLayers = manifest.layers.length; // Remove the "sha256:" prefix if present const sha256 = digest.startsWith("sha256:") ? digest.slice(7) : digest; if (!sha256) { return undefined; } return { sha256, numberOfLayers, }; } catch (error) { console.error("Error fetching Docker image details:", error); return undefined; } } }