TypeScript SDK
The reference implementation of the Aroha Protocol. 20 packages covering every protocol primitive — transport, identity, orchestration, credentials, settlement, reputation, and more.
Quick start — @aroha-sdk/run
The fastest path to a running agent. @aroha-sdk/run is a zero-dependency package that wraps any function into an Aroha-compatible HTTP server. No identity keys, no envelopes, no ceremony.
npm install @aroha-sdk/runimport { serve } from "@aroha-sdk/run";
const agent = serve("My Agent", async ({ message }) => {
return `Echo: ${message}`; // return a string, or yield chunks for streaming
});
agent.start(8000);
// POST http://localhost:8000/v1/run { message, sessionId?, stream? }
// GET http://localhost:8000/health
// GET http://localhost:8000/.well-known/aroha-agent.jsonagent.fetch as the default handler instead of calling agent.start(). It is a standard Web API Request → Response function.Streaming
const agent = serve("Streaming Agent", async function* ({ message }) {
const words = `Echo: ${message}`.split(" ");
for (const word of words) {
yield word + " ";
await new Promise((r) => setTimeout(r, 50));
}
});Bearer auth
const agent = serve("Secure Agent", handler, {
auth: "bearer",
bearerTokens: [process.env.AGENT_TOKEN!],
// or: verifyBearer: async (token) => myDb.isValid(token),
});
// Throws at startup if auth="bearer" and no tokens are configured.Signed callers (aroha-sig)
A bearer token proves someone holds a secret, not who they are — every holder of a leaked token is indistinguishable. auth: "aroha-sig" requires each request to be Ed25519-signed by the calling agent's DID key, so you can restrict access to named agents. Requires @aroha-sdk/run@1.3.0.
const agent = serve("Private Agent", handler, {
auth: "aroha-sig",
allowedCallers: ["did:aroha:alice", "did:aroha:ci-pipeline"],
// resolveCallerKey defaults to the Aroha registry.
// Omit allowedCallers to accept any verifiable caller (attribution only).
});Callers pass their identity and the SDK signs each request:
import { callAgent, streamAgent } from "@aroha-sdk/run";
const { message } = await callAgent(didHash, "Hello!", {
signAs: { did: "did:aroha:alice", privateKey: alicePrivateKey },
});
// streamAgent takes the same optionThe wire format matches the Python SDK exactly, so either language can call the other. See Private Agents for the full model, including registry visibility and access grants.
Hub registration + callAgent
// Register
const { didHash } = await agent.register("https://my-agent.fly.dev", {
apiKey: process.env.AROHA_API_KEY, // from Studio → API Keys
});
// Call any agent by didHash
import { callAgent } from "@aroha-sdk/run";
const { message, sessionId } = await callAgent(didHash, "Hello!", {
bearerToken: process.env.THEIR_TOKEN,
});MCP servers and skills
The registry indexes MCP servers and skills alongside agents, discoverable the same way. Both require an explicit did — the registry rejects a registration without one — generated for you if omitted. Requires @aroha-sdk/run@1.3.1.
import { registerMcp, resolveMcp, listMcps } from "@aroha-sdk/run";
const { didHash } = await registerMcp({
name: "search-mcp",
endpointUrl: "https://mcp.example.com",
tools: ["search", "fetch"], // names, or full { name, inputSchema } objects
apiKey: process.env.AROHA_API_KEY!,
});
const mcp = await resolveMcp(didHash);
const all = await listMcps({ limit: 20 });Skills use the same pattern. skillType must be one of the values in SKILL_TYPES (rag, web-search, memory, code, data, custom — default).
import { registerSkill } from "@aroha-sdk/run";
await registerSkill({
name: "my-rag-skill",
skillType: "rag",
apiKey: process.env.AROHA_API_KEY!,
});Registered MCP servers keep their own auth model — a Gmail or Stripe MCP still needs the calling agent to hold a valid OAuth token or API key for that vendor. Registration makes a server discoverable and correctly wired, not credential-free.
Calling agents
Use callAgent to call any Hub-registered agent by its didHash. The helper resolves the endpoint from the registry and POSTs to /v1/run automatically.
import { callAgent, listAgents, resolveAgent } from "@aroha-sdk/run";
// Call an agent
const { message } = await callAgent("abc123", "What is the capital of France?");
// List agents (optional org filter)
const agents = await listAgents({ org: "studio:abc123", limit: 20 });
// Resolve metadata only
const record = await resolveAgent("abc123");
console.log(record.endpointUrl, record.manifestCID);Installation — full SDK
Install only what you need. Every package is independently versioned.
# Minimum — agent transport + identity
npm install @aroha-sdk/core
# Full stack — add orchestration, auth, and payment
npm install @aroha-sdk/core @aroha-sdk/orchestrator @aroha-sdk/credentials @aroha-sdk/settlement
# Personal agent bridges
npm install @aroha-sdk/hermes-bridge # Hermes Agent / ZeroClaw (MCP stdio)
npm install @aroha-sdk/openclaw-bridge # OpenClaw skills
npm install @aroha-sdk/composio-bridge # TrustClaw / Composio actions
# Framework bridges
npm install @aroha-sdk/langchain-bridge # LangChain + AutoGen
npm install @aroha-sdk/mcp-bridge # Model Context Protocol
npm install @aroha-sdk/a2a-bridge # Google A2A"type": "module" in your package.json or use .mjs extensions.npm audit --omit=optional to see only warnings relevant to packages you install directly.npm run build --workspaces, or per-package: cd packages/aroha-core && npm run build. The compiled output lands in dist/ — the main field in each package.json points there.Core: ArohaServer & ArohaClient
@aroha-sdk/core is the only required package. It handles the full protocol stack: HTTP transport, Ed25519 envelope signing/verification, nonce replay protection, and WebSocket streaming.
Minimal agent (devMode)
import { ArohaServer, generateDid, generateKeyPair, toMultibase, MapNonceStore } from "@aroha-sdk/core";
// Generate a keypair and DID (persist these in production)
const { privateKey, publicKey } = await generateKeyPair();
const myDID = generateDid(publicKey);
const server = new ArohaServer({
agentDID: myDID,
didDocument: {
"@context": ["https://www.w3.org/ns/did/v1"],
id: myDID,
verificationMethod: [{
id: `${myDID}#key-1`,
type: "Ed25519VerificationKey2020",
controller: myDID,
publicKeyMultibase: toMultibase(publicKey), // base58btc multibase ('z' prefix)
}],
},
port: 3000,
devMode: true, // skip crypto in development (NODE_ENV=development required)
onMessage: async (envelope, respond, stream) => {
console.log("Received:", envelope.type, "from", envelope.from);
// respond() sends a synchronous reply
// stream() pushes events over WebSocket
},
resolvePublicKey: async (did) => {
// Return the sender's Ed25519 public key bytes, or null if unknown
return null;
},
});
await server.start();
console.log(`Agent ${myDID} listening on port 3000`);Production server (full crypto)
import { ArohaServer, generateDid, generateKeyPair } from "@aroha-sdk/core";
import { createRbacMiddleware } from "@aroha-sdk/credentials";
const { privateKey, publicKey } = await generateKeyPair();
const myDID = generateDid(publicKey);
const server = new ArohaServer({
agentDID: myDID,
didDocument: { /* ... */ },
port: 3000,
resolvePublicKey: async (did) => {
// Fetch from your registry or cache
const doc = await fetch(`https://aroha-registry.aroha-labs.workers.dev/dids/${did}`);
const { publicKey } = await doc.json();
return Buffer.from(publicKey, "base64");
},
middleware: [
// RBAC: require the caller to hold a valid SpendingMandate
createRbacMiddleware({ requiredTrustLevel: 2 }),
],
clockToleranceMs: 5000, // allow 5s clock skew across regions
maxBodyBytes: 1_048_576, // reject payloads > 1 MiB
minClientVersion: "1.0", // reject old client versions
onMessage: async (envelope, respond) => {
if (envelope.type === "ArohaRequest") {
const { capability, params } = envelope.body;
// handle capability...
respond(await buildEnvelope("ArohaResponse", myDID, envelope.from,
{ capability, result: { ok: true } },
envelope.correlationId, privateKey));
}
},
});
await server.start();ArohaClient — calling other agents
import { ArohaClient, buildEnvelope, newCorrelationId } from "@aroha-sdk/core";
const client = new ArohaClient();
const envelope = await buildEnvelope(
"ArohaRequest",
myDID, // from
"did:aroha:travel-agent", // to
{ capability: "search-flights", params: { from: "JFK", to: "LHR", date: "2026-08-01" } },
newCorrelationId(), // unique ID for this request
privateKey // signs the envelope
);
const response = await client.send("http://travel-agent.example.com", envelope);
if (response?.type === "ArohaResponse") {
console.log(response.body.result); // { flights: [...] }
}
if (!response) {
// Agent returned 202 — response will arrive via WebSocket stream
}Identity & DIDs
import { generateKeyPair, generateDid, buildWebDID } from "@aroha-sdk/core";
// did:aroha: (self-sovereign, content-addressed)
const { privateKey, publicKey } = await generateKeyPair();
const did = generateDid(publicKey);
// → "did:aroha:base58encodedPublicKey"
// did:aroha-web: (domain-anchored, like did:web:)
const webDID = buildWebDID("myco.ai", ["agents", "travel"]);
// → "did:aroha-web:myco.ai:agents:travel"
// Serve the DID document at /.well-known/aroha/agents/travel/did.json
// The ArohaServer does this automatically when webDIDDocument is set.Envelopes & Message Types
Every Aroha message is a signed envelope. The 16 built-in types map to the full protocol lifecycle:
import { buildEnvelope, newCorrelationId } from "@aroha-sdk/core";
// All envelope fields are set automatically:
// id → urn:uuid:<random>
// created → now
// expires → now + 5 min (default)
// nonce → random 16 bytes (base64url)
// proof → Ed25519 signature over canonical JSON
const env = await buildEnvelope(
"ArohaReserve", // type
orchestratorDID, // from
providerDID, // to
{
capability: "book-flight",
params: { from: "JFK", to: "LHR" },
budgetUsd: 500,
},
newCorrelationId(), // correlationId (ties Reserve→Commit→Cancel)
orchestratorPrivateKey
);SagaEngine & Orchestration
The @aroha-sdk/orchestrator package provides atomic multi-agent transactions using the saga pattern: Reserve → Commit → Cancel (LIFO compensation on failure).
import { SagaEngine, AgentSelector } from "@aroha-sdk/orchestrator";
import { ArohaClient } from "@aroha-sdk/core";
const saga = new SagaEngine({ client, orchestratorDID, privateKey });
// Each step is: reserve with one agent, then another, then commit all or cancel all
const result = await saga.run([
{
agentDID: "did:aroha:flight-agent",
endpoint: "http://flights.example.com",
capability: "reserve-flight",
params: { from: "JFK", to: "LHR", date: "2026-08-01" },
budgetUsd: 300,
},
{
agentDID: "did:aroha:hotel-agent",
endpoint: "http://hotels.example.com",
capability: "reserve-hotel",
params: { city: "London", nights: 3 },
budgetUsd: 200,
},
]);
// If hotels.example.com fails: flight reservation is automatically cancelled
// result.status → "committed" | "compensated"
// result.steps[n].commitToken → use for billing reconciliationAgentSelector — pick the best agent
import { AgentSelector } from "@aroha-sdk/orchestrator";
import { ArohaHttpRegistry } from "@aroha-sdk/registry";
const registry = new ArohaHttpRegistry("https://aroha-registry.aroha-labs.workers.dev");
const selector = new AgentSelector(registry);
// Discovers agents with "search-flights", ranks by reputation + latency + price
const best = await selector.select("search-flights", {
maxLatencyMs: 2000,
maxPriceUsd: 0.01,
minTrustLevel: 2,
});
console.log(best.manifest.did, best.endpoint);Credentials & RBAC
import {
issueIntentMandate,
verifyMandate,
createRbacMiddleware,
attenuateToCart,
attenuateToPayment,
ArohaRole,
} from "@aroha-sdk/credentials";
// Issue a mandate: user authorises the orchestrator to spend up to $500
// Signature: issueIntentMandate(grantorDID, granteeDID, constraints, privateKey, ttlMs?)
const mandate = await issueIntentMandate(
userDID,
orchestratorDID,
{ spendLimitUsd: 500, currency: "USD" },
userPrivateKey,
3_600_000, // 1 hour TTL
);
// mandate → { mandate, signature, token }
// Attenuate step 1: narrow to a specific provider (must be ≤ original limits)
// attenuateToCart(parent, granteeDID, narrowedConstraints, grantorPrivateKey, ttlMs?)
const cartMandate = await attenuateToCart(
mandate,
flightAgentDID,
{ spendLimitUsd: 300, currency: "USD" },
orchestratorPrivateKey,
);
// Attenuate step 2: lock down to a single payment amount
// attenuateToPayment(parent, granteeDID, narrowedConstraints, grantorPrivateKey, ttlMs?)
const payMandate = await attenuateToPayment(
cartMandate,
flightAgentDID,
{ spendLimitUsd: 299 },
orchestratorPrivateKey,
);
// Verify on the receiver side
// verifyMandate(token, publicKey, expectedGrantee?) → { valid, mandate?, reason? }
const { valid, mandate: decoded, reason } = await verifyMandate(
payMandate.token,
userPublicKey,
);
// On the server — enforce RBAC on every inbound message
const agentRoles = new Map([
[orchestratorDID, [ArohaRole.AgentOrchestrator]],
]);
const server = new ArohaServer({
middleware: [
createRbacMiddleware({
resolveAgentRoles: async (did) => agentRoles.get(did) ?? [],
}),
],
// ...
});Settlement
import { StripeSettlement, NullSettlement, EscrowSettlement, QuotaSettlement } from "@aroha-sdk/settlement";
import { SagaEngine } from "@aroha-sdk/orchestrator";
// Stripe backend — authorises on Reserve, captures on Commit, voids on Cancel
const settlement = new StripeSettlement({
secretKey: process.env.STRIPE_SECRET_KEY!,
resolveCharge: async (ctx) => {
if (ctx.capability === "book-flight")
return { amountCents: 45000, currency: "usd", customerId: "cus_abc" };
return null; // free capability
},
});
// Inject into SagaEngine — settlement is a saga concern, not a server concern
const saga = new SagaEngine({
client,
orchestratorDID: identity.did,
privateKey: identity.privateKey,
settlement, // ← here
});
// Available backends:
// NullSettlement — free tier / dev (default)
// QuotaSettlement — internal credit system
// StripeSettlement — manual-capture card charges
// EscrowSettlement — hold funds in escrow until Commit
// ApiKeySettlement — debit a pre-paid API credit balance
// AP2Settlement — ES256 SD-JWT mandates for Google's Agent Payments Protocol (alpha)Agent Registry
import { InMemoryRegistry, ArohaHttpRegistry } from "@aroha-sdk/registry";
// Development: in-process registry
const registry = new InMemoryRegistry();
registry.register({
did: myDID,
endpoint: "http://localhost:3000",
capabilities: [
{ id: "search-flights", description: "Search available flights" },
],
});
// Production: federated HTTP registry
const registry = new ArohaHttpRegistry("https://aroha-registry.aroha-labs.workers.dev");
// Discover agents
const agents = await registry.find({ capability: "search-flights" });
// → [{ manifest: { did, capabilities }, endpoint }]Framework Bridges
Hermes Agent & ZeroClaw (MCP stdio)
// hermes.config.json (or zeroclaw config.toml)
{
"mcpServers": {
"aroha-travel": {
"command": "npx",
"args": [
"@aroha-sdk/hermes-bridge",
"--endpoint", "http://travel-agent.example.com",
"--agent-did", "did:aroha:travel-agent"
]
}
}
}OpenClaw
// openclaw-plugin.mjs
import { createArohaOpenClawPlugin } from "@aroha-sdk/openclaw-bridge";
import { generateKeyPair, generateDid } from "@aroha-sdk/core";
const { privateKey, publicKey } = await generateKeyPair();
const myDID = generateDid(publicKey);
export default createArohaOpenClawPlugin("aroha-travel", [
{
capabilityId: "search-flights",
endpoint: "http://travel-agent.example.com",
agentDID: "did:aroha:travel-agent",
callerDID: myDID,
callerPrivateKey: privateKey,
description: "Search and book flights via Aroha travel agent",
parameters: {
from: { type: "string", description: "Origin IATA code (e.g. JFK)" },
to: { type: "string", description: "Destination IATA code (e.g. LHR)" },
date: { type: "string", description: "Departure date YYYY-MM-DD" },
},
},
]);Composio / TrustClaw
import { Composio } from "composio-core";
import { registerArohaCapabilities } from "@aroha-sdk/composio-bridge";
import { generateKeyPair, generateDid } from "@aroha-sdk/core";
const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });
const { privateKey, publicKey } = await generateKeyPair();
const myDID = generateDid(publicKey);
await registerArohaCapabilities(composio, [
{
endpoint: "http://travel-agent.example.com",
agentDID: "did:aroha:travel-agent",
capabilityId: "search-flights",
description: "Search for available flights",
callerDID: myDID,
callerPrivateKey: privateKey,
parameters: {
from: { type: "string", description: "Origin IATA code" },
to: { type: "string", description: "Destination IATA code" },
},
required: ["from", "to"],
},
]);
// Now TrustClaw, Claude+Composio, GPT-4+Composio can all call thisLangChain / AutoGen
import { arohaCapabilityToLangChainTool, arohaCapabilityToOpenAITool } from "@aroha-sdk/langchain-bridge";
// LangChain tool
const flightTool = arohaCapabilityToLangChainTool("search-flights", {
endpoint: "http://travel-agent.example.com",
agentDID: "did:aroha:travel-agent",
callerDID: myDID,
callerPrivateKey: privateKey,
description: "Search available flights between airports",
});
// AutoGen / OpenAI function tool
const flightFn = arohaCapabilityToOpenAITool("search-flights", { /* same opts */ });
// Pass to your LangChain agent as a normal tool
const agent = initializeAgentExecutorWithOptions([flightTool], llm, { agentType: "openai-functions" });