@silvana-one/coordination
Version:
Silvana Coordination Client
865 lines (766 loc) • 23.3 kB
text/typescript
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;
}
}
}