Agent Management

Manage instances of mobile agents encapsulating DI protocol engagements and wallet data

The Edge Agent SDK revolves around instances of SudoDIEdgeAgents. Agent instances provide access to modules for accessing data or DI protocol engagements. As well as APIs for managing the agent's configuration, managing the message receiver loop, and hooks for subscribing to agent events.

Initializing an Agent

Configuration

Before initializing an agent, an AgentConfiguration object must be created to dictate some of the agent's behavior. Using the fields in AgentConfiguration, the following agent behavior can be configured:

Required:

  • genesisFiles field of the configuration must be provided. It should contain a list of Decentralized Identity ledger genesis files (indy-based ledger pool transaction genesis files) stored on the device. It is the responsibility of the application consumer to manage the ledger files used by the Agent.

Please note that currently only one genesis file at a time is currently supported. Multiple simultaneous ledgers will be supported in the future. Therefore, genesisFiles should be provided as a list of exactly one single genesis file.

Optional:

  • storageDirectory can be used to specify a directory location on the device where agent data will be stored. If null is provided (or if unprovided), a persistent data directory within the application's environment will be used.

  • peerConnectionConfiguration is the default configuration for how the agent should present itself to peers it connects with. It includes a label field for a self-assigned unverified label or nickname.

  • exchangePreservationConfiguration controls preservation of exchange records in the wallet after a protocol (connection, credential or proof presentation protocols) is complete.

  • cacheConfiguration controls how the agent should cache different data it uses (such as immutable data it receives from the ledger it uses).

  • networkConfiguration controls how the agent should handle different network interactions. For instance, networkConfiguration can be used to configure how many times the agent should retry any network timeouts it receives from the ledger, which is useful when the agent is using a ledger with imperfect node reception.

  • + more. The configurable parameters on the agent are growing over time. To see the full set of configurable fields, please refer to the API Reference.

Builder

To initialize a SudoDIEdgeAgent, the associated Builder class can be used along with the configuration you wish to use (see above).

An AgentConfiguration must be set in the builder. A logger can also optionally be set.

The following example demonstrates how to build an agent using a default configuration.

let genesisFiles: [URL] // a list of a URL pointing to a indy-ledger genesis file on disk

let agentConfiguration = AgentConfiguration(genesisFiles: genesisFiles)
agent = try SudoDIEdgeAgentBuilder()
            .setAgentConfiguration(agentConfiguration: agentConfiguration)
            .build()

Changing Agent Configuration

After an agent is initialized, the configuration can be changed on the fly if desired. This can be achieved via the setAgentConfiguration API. This will allow certain behavior, such as credential auto acceptance, agent connection appearance, and more to be changed.

let agent: SudoDIEdgeAgent // agent to change configuration for
let newConfiguration: AgentConfiguration // the new configuration to set the agent to

try agent.setAgentConfiguration(agentConfiguration: newConfiguration)

Some configuration fields should not be changed whilst an agent has a wallet open. The SDK inline code documentation notes these fields. These fields include storageDirectory and genesisFiles. To reconfigure these fields on an agent, it is recommended to stop the agent's run loop, and then close the wallet before setting a new config for these values.

Receiving a URL Request

As part of the protocols supported by the Edge Agent, external requests (from Issuers, Verifiers, etc) are often made to the Wallet App in the form of a URL request. For instance, a deep link to the app or a QR Code scanned by the app. The Edge Agent API receiveUrl can be used to process these incoming URL requests.

The following request types are supported:

When a URL is successfully processed and received by the agent, the agent's data state is updated, and a corresponding event may trigger (see Subscribe to Events). After this, and depending on the type of request received, the agent may be ready to respond to the request via protocol interactions (refer to later relevant sections in this documentation such as Establishing Connections, Accepting New Credentials & Present Credentials for Verification).

let agent: SudoDIEdgeAgent
let requestUrl: String // string URL received from some channel (QR Code, deep link)

do {
    try await agent.receiveUrl(url: requestUrl)
} catch {
    // handle error
}

Most URLs will only be processable if the agent has their wallet storage open before receiving. Please see the documentation on wallet management.

How URL requests are captured before being passed into the receiveUrl API is a responsibility of the application itself. The application may wish to register known deep link schemes for the application, or implement a QR Code scanner.

See our sample apps as a reference.

Receiving a DIDComm Message

With an instantiated SudoDIEdgeAgent, you are ready to begin receiving and processing DIDComm messages. The agent API receiveMessage can be used to receive supported Aries RFC DIDComm messages. Typically, the messages passed into receiveMessage are encrypted (Aries RFC 0019) and come from some communication channel with a peer (e.g. via a Relay or Mediator source). However, some non-encrypted messages are supported as well, most notably Connection invitations received through some other channel (e.g. out of band channels).

let agent: SudoDIEdgeAgent
let rawMessage: Data // bytes of a raw DIDComm message received from some channel
let metadata: ReceivedMessageMetadata // metadata of the received DIDComm message. Usually generated by the message source

do {
    try await agent.receiveMessage(
        rawMessage: rawMessage,
        metadata: metadata
    )
} catch {
    // handle error
}

Most messages will only be processable if the agent has their wallet storage open before receiving a message. Please see the documentation on wallet management.

The receiveMessage API allows for manual processing of messages to be achieved. If automatic message receiving and processing is desired, configuring a MessageSource and using the agent run method will be more fitting for this use case (see below).

Running Agent Management

In addition to manually receiving DIDComm messages, the agent can be run in a loop to poll for and process messages that come in via a MessageSource. Information on MessageSources and using the default provided implementations can be found below.

Note that similar to receiveMessage, the agent must have a wallet open before starting the run loop. Additionally, only one run loop for an agent can be run at once.

The run loop performs any pre-checks and then begins the loop (polling the MessageSource in a background task). The API can be called like so:

let agent: SudoDIEdgeAgent
let messageSource: MessageSource // some implementation of a MessageSource

do {
    try agent.run(messageSource: messageSource)
} catch {
    // handle error
}

After an agent is running, several APIs can be used to manage the run loop. These APIs are:

  • isRunning(): Boolean - reports whether the agent is actively running and not attempting to stop. False indicates that the loop is stopped.

  • stop() - triggers the agent to begin its stop sequence (finishing whatever message it was processing, if any)

  • isStopping(): Boolean - reports whether the agent is in the stop sequence (as requested via stop()).

Message Sources

The agent's run loop is powered by implementations of the MessageSource interface. As a consumer, you can choose to implement your own message source, or use our provided implementations.

To implement your own MessageSource, all you need to implement is 2 methods - getMessage(): Message? and finalizeMessage(id: String). Quite simply, your implementation of getMessage should attempt to fetch the next available Message from your message source channel, and then finalizeMessage should take the provided id (from Message.id) and make the necessary action to 'finalize' or remove the message from the message source). The message body returned by the getMessage call should encrypted or unencrypted Aries DIDComm messages.

Our provided MessageSources, particularly Relay Message Source, can be used as a reference for designing your own MessageSource.

Relay Message Source

To integrate the Edge Agent SDK with our Sudo DI Relay SDK , you can use the SudoDIRelayMessageSource implementation. The SudoDIRelayMessageSource can be created using the constructor:

let relayClient: SudoDIRelayClient
let logger: Logger

let messageSource = SudoDIRelayMessageSource(relayClient: relayClient, logger: logger)

do {
    try agent.run(messageSource: messageSource)
} catch {
    // handle error
}

To get a SudoDIRelayClient instance, please refer to the Sudo DI Relay SDK integration documentation

Other Provided Message Sources

In addition to the relay message source, you may use a URL DIDComm message processing message source (UrlMessageSource) and a simple aggregator message source (RoundRobinMultiMessageSource).

The UrlMessageSource can be used to parse and queue DIDComm messages that are encoded in a URL (i.e. an Out of Band 0434 Message, or a Connection Protocol 0160 Invitation). These messages can be passed into the queueUrlMessage method to queue them for usage in the agent:

let logger: Logger 
let urlEncodedMessage: String // (e.g. from scanning an invitation QR code)

let messageSource = UrlMessageSource(logger: logger)

do {
    try agent.run(messageSource: messageSource)
} catch {
    // handle error
}

do {
    try await messageSource.queueUrlMessage(url: urlEncodedMessage)
} catch {
    // handle error
}

The RoundRobinMultiMessageSource can be used to aggregate together multiple message sources. As the name implies, the implementation simply polls the MessageSources it has been given in a fair round-robin approach. For more use-case specific aggregations of MessageSources, a custom implementation may be more suitable.

The RoundRobinMultiMessageSource can be used like so:

let urlMessageSource: UrlMessageSource
let relayMessageSource: SudoDIRelayMessageSource
let logger: Logger

let multiMessageSource = RoundRobinMultiMessageSource(logger: logger)
multiMessageSource.addMessageSource(messageSource: urlMessageSource)
multiMessageSource.addMessageSource(messageSource: relayMessageSource)

do {
    try agent.run(messageSource: multiMessageSource)
} catch {
    // handle error
}

Subscribe to Events

It is important for consuming applications to be aware of updates to the agent's protocol states in real-time, rather than polling for updates. Important protocol state updates and events include:

  • A new incoming Connection establishment request, or an update to an existing Connection establishment protocol

  • A new incoming Credential offer from a credential Issuer, or an update to an existing Credential exchange protocol

  • A new incoming Credential Presentation request from a credential Verifier, or an update to an existing Presentation exchange protocol

  • A new incoming basic message from a connection

These state changes can be subscribed to via the subscribeToAgentEvents API. This API takes an implementation of AgentEventSubscriber, which has methods that will be invoked on different event types.

The subscribeToAgentEvents API can be called multiple times to attach many subscribers. Each call to the API will return a unique String identifier, which can be passed into unsubscribeToAgentEvents in order to remove the subscriber from receiving event updates. unsubscribeAll is an API that can also be used to quickly remove all subscribers from receiving event updates.

class CustomSubscriber : AgentEventSubscriber {
    func connectionExchangeStateChanged(connectionExchange: ConnectionExchange) {
        // the following will be invoked whenever a connection protocol state change occurs.
        // ConnectionExchange will contain the updated state
        print("connection update: \(connectionExchange)")
    }
    func credentialExchangeStateChanged(credentialExchange: CredentialExchange) {
        // the following will be invoked whenever a credential issuance protocol state change occurs.
        // CredentialExchange will contain the updated state
        print("credential update: \(credentialExchange)")
    }
    func proofExchangeStateChanged(proofExchange: ProofExchange) {
        // the following will be invoked whenever a proof presentation protocol state change occurs.
        // ProofExchange will contain the updated state
        print("proof update: \(proofExchange)")
    }
    func inboundBasicMessage(basicMessage: BasicMessage.Inbound) {
        // the following will be invoked whenever a new basic message is received.
        print("new message: \(basicMessage)")
    }
    func messageProcessed(messageId: String) {
        // the following will be invoked whenever the agent run loop processes a new message.
        print("message processed: \(messageId)")
    }
}


let customSubscriber = CustomSubscriber()
let subscriberId = agent.subscribeToAgentEvents(subscriber: customSubscriber)
let subscriberId: String // captured from `subscribeToAgentEvents`
agent.unsubscribeToAgentEvents(subscriptionId: subscriberId)
agent.unsubscribeAll()

Managing Ledger Cache

As a decentralized identity agent, lots of read requests to ledgers to retrieve immutable data are performed. These read requests can be costly depending on the reception of nodes on the ledger network. To help reduce this cost, the agent features APIs to help management the caching of these ledger read requests. As mentioned above, the agent configuration contains a field to turn on or off caching of immutable ledger data. By default, caching is turned on.

If caching is being used, you can clear the cache at your own discretion via the clearLedgerCache API. This API will succeed regardless of whether any cache was cleared or not.

let agent: SudoDIEdgeAgent

do {
    try await agent.clearLedgerCache()
} catch {
    // handle error
}

Last updated