Back to Docs
Jv

Java SDK

v1.2.0 · MIT License · Java 21+ · Bouncy Castle

Java 21 records and sealed interfaces throughout. Ed25519 via Bouncy Castle, async HTTP via Java 11 HttpClient, Spring AI bridge included.

Installation

Gradle

dependencies {
    implementation 'io.aroha:aroha-sdk:1.0.0'
}

Maven

<dependency>
  <groupId>io.aroha</groupId>
  <artifactId>aroha-sdk</artifactId>
  <version>1.0.0</version>
</dependency>
Java 21+ required. The SDK uses virtual threads (Thread.ofVirtual()) for non-blocking I/O without a reactive framework dependency.

ArohaServer

Minimal agent

import io.aroha.transport.ArohaServer;
import io.aroha.transport.ArohaServerOptions;
import io.aroha.messages.ArohaEnvelope;
import io.aroha.identity.DidUtils;
import java.security.KeyPair;

// Generate keypair and DID (persist in production)
KeyPair keyPair = DidUtils.generateKeyPair();
String myDID    = DidUtils.generateDid(keyPair.getPublic());

var server = ArohaServer.builder()
    .agentDID(myDID)
    .port(3000)
    .devMode(true)   // skip crypto in development
    .onMessage((envelope, respond, stream) -> {
        if ("ArohaRequest".equals(envelope.type())) {
            var body       = envelope.body();
            var capability = (String) body.get("capability");
            var params     = (Map<String, Object>) body.get("params");

            if ("greet".equals(capability)) {
                respond.accept(ArohaResponse.of(
                    capability,
                    Map.of("message", "Hello, " + params.get("name") + "!")
                ));
            }
        }
    })
    .resolvePublicKey(did -> Optional.empty())   // return the sender's public key bytes
    .build();

server.start();
System.out.println("Agent " + myDID + " listening on :3000");

Production server

import io.aroha.transport.ArohaServer;
import io.aroha.credentials.RbacMiddleware;

var server = ArohaServer.builder()
    .agentDID(myDID)
    .privateKey(keyPair.getPrivate())
    .port(3000)
    .clockToleranceMs(5000)
    .maxBodyBytes(1_048_576)
    .minClientVersion("1.0")
    .resolvePublicKey(did -> registry.lookupPublicKey(did))
    .middleware(List.of(
        RbacMiddleware.ofTrustLevel(2)
    ))
    .onMessage(this::handleMessage)
    .build();

server.start();

ArohaClient

import io.aroha.transport.ArohaClient;
import io.aroha.messages.ArohaEnvelopeBuilder;

var client = new ArohaClient();

var envelope = ArohaEnvelopeBuilder.builder()
    .type("ArohaRequest")
    .from(myDID)
    .to("did:aroha:travel-agent")
    .body(Map.of(
        "capability", "search-flights",
        "params", Map.of("from", "JFK", "to", "LHR", "date", "2026-08-01")
    ))
    .correlationId(ArohaEnvelopeBuilder.newCorrelationId())
    .privateKey(keyPair.getPrivate())
    .build();

var response = client.send("http://travel-agent.example.com", envelope);
response.ifPresent(resp -> {
    if ("ArohaResponse".equals(resp.type())) {
        System.out.println(resp.body().get("result"));
    }
});

Identity & DIDs

import io.aroha.identity.DidUtils;
import java.security.KeyPair;

// Self-sovereign DID
KeyPair keyPair = DidUtils.generateKeyPair();
String did = DidUtils.generateDid(keyPair.getPublic());
// → "did:aroha:base58encodedPublicKey"

// Domain-anchored DID
String webDid = DidUtils.buildWebDid("myco.ai", "agents", "travel");
// → "did:aroha-web:myco.ai:agents:travel"

// Resolve a DID to its document
DidDocument doc = DidUtils.resolve("did:aroha:someagent");
byte[] publicKeyBytes = doc.verificationMethods().get(0).publicKeyBytes();

Envelopes

import io.aroha.messages.ArohaEnvelopeBuilder;
import io.aroha.messages.ArohaEnvelope;
import io.aroha.messages.ValidationResult;

// Build — all fields set automatically (id, created, expires, nonce, proof)
ArohaEnvelope env = ArohaEnvelopeBuilder.builder()
    .type("ArohaReserve")
    .from(orchestratorDID)
    .to(providerDID)
    .body(Map.of(
        "capability", "book-flight",
        "params",     Map.of("from", "JFK", "to", "LHR"),
        "budgetUsd",  500
    ))
    .correlationId(ArohaEnvelopeBuilder.newCorrelationId())
    .privateKey(privateKey)
    .ttlSeconds(300)
    .build();

// Validate an inbound envelope
ValidationResult result = ArohaEnvelopeValidator.validate(
    env, senderPublicKey, myDID, nonceRegistry,
    ValidationOptions.withClockToleranceMs(5000)
);
if (!result.valid()) {
    throw new ArohaException(result.reason());
}

Spending Mandates

import io.aroha.mandate.ArohaMandateCredential;
import io.aroha.mandate.ArohaMandateCredential.*;

// Define constraints
var constraints = new SpendingConstraints.Builder()
    .spendLimitUsd(500.0)
    .allowedCapabilities(List.of("search-flights", "reserve-flight"))
    .expiresAt(Instant.now().plus(Duration.ofHours(1)))
    .build();

// User issues a mandate to the orchestrator
var mandate = ArohaMandateCredential.issueIntent(
    userDID,
    orchestratorDID,
    constraints,
    userPrivateKey
);

// Orchestrator attenuates to a sub-agent
var subMandate = ArohaMandateCredential.attenuate(
    mandate,
    flightAgentDID,
    new SpendingConstraints.Builder()
        .spendLimitUsd(300.0)                         // ≤ 500
        .allowedCapabilities(List.of("reserve-flight"))
        .build(),
    orchestratorPrivateKey
);

// Verify on the receiving agent
var verifyResult = ArohaMandateCredential.verify(subMandate, flightAgentDID);
System.out.println(verifyResult.valid());   // true
System.out.println(verifyResult.reason());  // "ok"

Saga Orchestration

import io.aroha.orchestrator.SagaEngine;
import io.aroha.orchestrator.SagaStep;
import io.aroha.orchestrator.SagaResult;

var engine = SagaEngine.builder()
    .client(client)
    .orchestratorDID(myDID)
    .privateKey(privateKey)
    .build();

var result = engine.run(List.of(
    SagaStep.builder()
        .agentDID("did:aroha:flight-agent")
        .endpoint("http://flights.example.com")
        .capability("reserve-flight")
        .params(Map.of("from", "JFK", "to", "LHR"))
        .budgetUsd(300)
        .build(),
    SagaStep.builder()
        .agentDID("did:aroha:hotel-agent")
        .endpoint("http://hotels.example.com")
        .capability("reserve-hotel")
        .params(Map.of("city", "London", "nights", 3))
        .budgetUsd(200)
        .build()
));

System.out.println(result.status());         // SagaStatus.COMMITTED or COMPENSATED
result.steps().forEach(s ->
    System.out.println(s.commitToken())      // use for billing
);

Spring AI Bridge

import io.aroha.bridges.SpringAiBridge;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.FunctionToolCallback;

// Wrap an Aroha capability as a Spring AI tool
FunctionToolCallback flightTool = SpringAiBridge.toSpringAiTool(
    "search-flights",
    SpringAiBridge.ArohaToolOptions.builder()
        .endpoint("http://travel-agent.example.com")
        .agentDID("did:aroha:travel-agent")
        .callerDID(myDID)
        .callerPrivateKey(privateKey)
        .description("Search for available flights between airports")
        .build()
);

// Use with Spring AI ChatClient
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(flightTool)
    .build();

String response = chatClient.prompt()
    .user("Find flights from JFK to London on August 1st")
    .call()
    .content();

System.out.println(response);

All Packages

io.aroha.cryptoEd25519 via Bouncy Castle, AES-256-GCM
io.aroha.identityDID generation, did:aroha-web:, resolution
io.aroha.messagesArohaEnvelope (Java 21 record), builder, validator, NonceRegistry
io.aroha.transportArohaServer (virtual threads), ArohaClient (HttpClient)
io.aroha.orchestratorSagaEngine, SagaStep, AgentSelector, CSNHandler
io.aroha.mandateArohaMandateCredential: issueIntent, attenuate, verify
io.aroha.reputationReputationEngine (Bayesian Beta)
io.aroha.bridgesSpringAiBridge — Spring AI FunctionToolCallback adapter