Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DIDComm V2 Interopathon #3329

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4806fab
feat: (WIP) add DCV2 protocol registry
mepeltier Nov 1, 2024
b57ad4f
feat: WIP Load in Protocol Registry V2
TheTechmage Nov 1, 2024
c25ce9a
feat: WIP Use Protocol Registry for DIDComm V2
TheTechmage Nov 1, 2024
da85fac
feat: Call into handlers to handle DIDCommV2 messages
TheTechmage Nov 1, 2024
bac01de
fix: WIP remove broken import
TheTechmage Nov 4, 2024
9966e8b
fix: Call method on instance, not class
TheTechmage Nov 4, 2024
a4748ea
feat: Add way to pull handler out of protocol registry
TheTechmage Nov 4, 2024
280e715
feat: import handlers if not already imported
TheTechmage Nov 21, 2024
02818e3
feat: convert handler to a class
TheTechmage Nov 21, 2024
7452d0e
feat: Add options response for DIDComm Demo
TheTechmage Nov 21, 2024
e1da1d2
feat: Respond to target list & msg fixes
TheTechmage Nov 21, 2024
a544951
chore: move destination logic to dispatcher
TheTechmage Dec 3, 2024
6f61aeb
feat: Add trust-ping protocol
TheTechmage Dec 4, 2024
d47001f
feat: Add admin route for trust ping
TheTechmage Dec 5, 2024
e56f6fe
feat: Add arguments to trust ping api
TheTechmage Dec 9, 2024
59c0059
feat: Send ping message to did from admin route
TheTechmage Dec 10, 2024
5dbaeb4
feat: simplify request handling in prep for more protocols
TheTechmage Dec 11, 2024
416bfae
feat: Add swagger tag to make interop-a-thon easier
TheTechmage Dec 11, 2024
6eed305
feat: Add discover features query
TheTechmage Dec 11, 2024
2da77c8
feat: Add Basic Message protocol
TheTechmage Dec 11, 2024
bf8c8f7
feat: Update OpenAPI routes
TheTechmage Dec 11, 2024
9d50e4a
fix: Broken function signature
TheTechmage Dec 11, 2024
01031f9
fix: Honor response_requested parameter
TheTechmage Dec 11, 2024
410553a
fix: switch to using did:peer:4
TheTechmage Dec 12, 2024
708de84
Undo: Not ready for peer 4 yet
TheTechmage Dec 12, 2024
be0dfee
chore: Cleanup PR
TheTechmage Dec 20, 2024
ebefef9
chore: Split out protocols from Trust Ping
TheTechmage Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions acapy_agent/config/default_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from ..core.plugin_registry import PluginRegistry
from ..core.profile import ProfileManager, ProfileManagerProvider
from ..core.protocol_registry import ProtocolRegistry
from ..didcomm_v2.protocol_registry import V2ProtocolRegistry
from ..protocols.actionmenu.v1_0.base_service import BaseMenuService
from ..protocols.actionmenu.v1_0.driver_service import DriverMenuService
from ..protocols.introduction.v0_1.base_service import BaseIntroductionService
Expand Down Expand Up @@ -45,6 +46,7 @@ async def build_context(self) -> InjectionContext:

# Global protocol registry
context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry())
context.injector.bind_instance(V2ProtocolRegistry, V2ProtocolRegistry())

# Global goal code registry
context.injector.bind_instance(GoalCodeRegistry, GoalCodeRegistry())
Expand Down Expand Up @@ -129,6 +131,7 @@ async def load_plugins(self, context: InjectionContext):
# Register standard protocol plugins
if not self.settings.get("transport.disabled"):
plugin_registry.register_package("acapy_agent.protocols")
plugin_registry.register_package("acapy_agent.protocols_v2")

# Currently providing admin routes only
plugin_registry.register_plugin("acapy_agent.holder")
Expand Down
172 changes: 160 additions & 12 deletions acapy_agent/core/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
from typing import Callable, Coroutine, Optional, Union

from aiohttp.web import HTTPException
from didcomm_messaging import DIDCommMessaging, RoutingService

from ..connections.base_manager import BaseConnectionManager
from ..connections.models.conn_record import ConnRecord
from ..connections.models.connection_target import ConnectionTarget
from ..core.profile import Profile
from ..messaging.agent_message import AgentMessage
from ..messaging.base_message import BaseMessage, DIDCommVersion
Expand All @@ -34,6 +36,7 @@
from ..utils.tracing import get_timer, trace_event
from .error import ProtocolMinorVersionNotSupported
from .protocol_registry import ProtocolRegistry
from ..didcomm_v2.protocol_registry import V2ProtocolRegistry


class ProblemReportParseError(MessageParseError):
Expand Down Expand Up @@ -137,31 +140,176 @@ async def handle_v2_message(
):
"""Handle a DIDComm V2 message."""

error_result = None
message = None

try:
message = await self.make_v2_message(profile, inbound_message.payload)
except ProblemReportParseError:
pass # avoid problem report recursion
except MessageParseError as e:
self.logger.error(f"Message parsing failed: {str(e)}, sending problem report")
error_result = ProblemReport(
description={
"en": str(e),
"code": "message-parse-failure",
}
)
if inbound_message.receipt.thread_id:
error_result.assign_thread_id(inbound_message.receipt.thread_id)

session = await profile.session()
ctx = session
messaging = ctx.inject(DIDCommMessaging)
routing_service = ctx.inject(RoutingService)
frm = inbound_message.payload.get("from")
services = await routing_service._resolve_services(messaging.resolver, frm)
chain = [
{
"did": frm,
"service": services,
}
]

# Loop through service DIDs until we run out of DIDs to forward to
to_did = services[0].service_endpoint.uri
found_forwardable_service = await routing_service.is_forwardable_service(
messaging.resolver, services[0]
)
while found_forwardable_service:
services = await routing_service._resolve_services(messaging.resolver, to_did)
if services:
chain.append(
{
"did": to_did,
"service": services,
}
)
to_did = services[0].service_endpoint.uri
found_forwardable_service = (
await routing_service.is_forwardable_service(messaging.resolver, services[0])
if services
else False
)
reply_destination = [
ConnectionTarget(
did=inbound_message.receipt.sender_verkey,
endpoint=service.service_endpoint.uri,
recipient_keys=[inbound_message.receipt.sender_verkey],
sender_key=inbound_message.receipt.recipient_verkey,
)
for service in chain[-1]["service"]
]

# send a DCV2 Problem Report here for testing, and to punt procotol handling down
# the road a bit
context = RequestContext(profile)
context.message = message
context.message_receipt = inbound_message.receipt
responder = DispatcherResponder(
context,
inbound_message,
send_outbound,
reply_session_id=inbound_message.session_id,
reply_to_verkey=inbound_message.receipt.sender_verkey,
target_list=reply_destination
)

context.injector.bind_instance(BaseResponder, responder)
error_result = V2AgentMessage(
message={
"type": "https://didcomm.org/report-problem/2.0/problem-report",
"body": {
"comment": "No Handlers Found",
"code": "e.p.msg.not-found",
},
}
)
if inbound_message.receipt.thread_id:
error_result.message["pthid"] = inbound_message.receipt.thread_id
await responder.send_reply(error_result)
if not message:
error_result = V2AgentMessage(
message={
"type": "https://didcomm.org/report-problem/2.0/problem-report",
"body": {
"comment": "No Handlers Found",
"code": "e.p.msg.not-found",
},
}
)
if inbound_message.receipt.thread_id:
error_result.message["pthid"] = inbound_message.receipt.thread_id

# # When processing oob attach message we supply the connection id
# # associated with the inbound message
# if inbound_message.connection_id:
# async with self.profile.session() as session:
# connection = await ConnRecord.retrieve_by_id(
# session, inbound_message.connection_id
# )
# else:
# connection_mgr = BaseConnectionManager(profile)
# connection = await connection_mgr.find_inbound_connection(
# inbound_message.receipt
# )
# del connection_mgr

# if connection:
# inbound_message.connection_id = connection.connection_id

# context.connection_ready = connection and connection.is_ready
# context.connection_record = connection
# responder.connection_id = connection and connection.connection_id

if error_result:
await responder.send_reply(error_result)
elif context.message:
context.injector.bind_instance(BaseResponder, responder)

handler = context.message
if self.collector:
handler = self.collector.wrap_coro(handler, [handler.__qualname__])
await handler()(context, responder, payload=inbound_message.payload)

async def make_v2_message(self, profile: Profile, parsed_msg: dict) -> BaseMessage:
"""Deserialize a message dict into the appropriate message instance.

Given a dict describing a message, this method
returns an instance of the related message class.

Args:
parsed_msg: The parsed message
profile: Profile

Returns:
An instance of the corresponding message class for this message

Raises:
MessageParseError: If the message doesn't specify @type
MessageParseError: If there is no message class registered to handle
the given type

"""
if not isinstance(parsed_msg, dict):
raise MessageParseError("Expected a JSON object")
message_type = parsed_msg.get("type")

if not message_type:
raise MessageParseError("Message does not contain 'type' parameter")

registry: V2ProtocolRegistry = self.profile.inject(V2ProtocolRegistry)
try:
#message_cls = registry.resolve_message_class(message_type)
#if isinstance(message_cls, DeferLoad):
# message_cls = message_cls.resolved
message_cls = registry.protocols_matching_query(message_type)
except ProtocolMinorVersionNotSupported as e:
raise MessageParseError(f"Problem parsing message type. {e}")

if not message_cls:
raise MessageParseError(f"Unrecognized message type {message_type}")

try:
#instance = message_cls[0] #message_cls.deserialize(parsed_msg)
instance = registry.handlers[message_cls[0]]
if isinstance(instance, DeferLoad):
instance = instance.resolved
except BaseModelError as e:
if "/problem-report" in message_type:
raise ProblemReportParseError("Error parsing problem report message")
raise MessageParseError(f"Error deserializing message: {e}") from e

return instance


async def handle_v1_message(
self,
Expand Down
7 changes: 7 additions & 0 deletions acapy_agent/core/plugin_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .error import ProtocolDefinitionValidationError
from .goal_code_registry import GoalCodeRegistry
from .protocol_registry import ProtocolRegistry
from ..didcomm_v2.protocol_registry import V2ProtocolRegistry

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -218,8 +219,14 @@ async def load_protocol_version(
version_definition: Optional[dict] = None,
):
"""Load a particular protocol version."""
v2_protocol_registry = context.inject(V2ProtocolRegistry)
protocol_registry = context.inject(ProtocolRegistry)
goal_code_registry = context.inject(GoalCodeRegistry)
if hasattr(mod, "HANDLERS"):
for message_type, handler in mod.HANDLERS:
v2_protocol_registry.register_handler(
message_type, handler
)
if hasattr(mod, "MESSAGE_TYPES"):
protocol_registry.register_message_types(
mod.MESSAGE_TYPES, version_definition=version_definition
Expand Down
43 changes: 43 additions & 0 deletions acapy_agent/didcomm_v2/protocol_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Registry for DIDComm V2 Protocols."""

from ..utils.classloader import DeferLoad
from typing import Coroutine, Dict, Sequence, Union


class V2ProtocolRegistry:
"""DIDComm V2 Protocols."""

def __init__(self):
"""Initialize a V2ProtocolRegistry instance."""
self._type_to_message_handler: Dict[str, Coroutine] = {}

@property
def handlers(self) -> Dict[str, Coroutine]:
"""Accessor for a list of all message protocols."""
return self._type_to_message_handler

@property
def protocols(self) -> Sequence[str]:
"""Accessor for a list of all message protocols."""
return [str(key) for key in self._type_to_message_handler.keys()]

def protocols_matching_query(self, query: str) -> Sequence[str]:
"""Return a list of message protocols matching a query string."""
all_types = self.protocols
result = None

if query == "*" or query is None:
result = all_types
elif query:
if query.endswith("*"):
match = query[:-1]
result = tuple(k for k in all_types if k.startswith(match))
elif query in all_types:
result = (query,)
return result or ()

def register_handler(self, message_type: str, handler: Union[Coroutine, str]):
"""Register a new message type to handler association."""
if isinstance(handler, str):
handler = DeferLoad(handler)
self._type_to_message_handler[message_type] = handler
5 changes: 4 additions & 1 deletion acapy_agent/messaging/responder.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ def __init__(
connection_id: Optional[str] = None,
reply_session_id: Optional[str] = None,
reply_to_verkey: Optional[str] = None,
target: Optional[ConnectionTarget] = None,
target_list: Sequence[ConnectionTarget] = None,
):
"""Initialize a base responder."""
self.connection_id = connection_id
self.reply_session_id = reply_session_id
self.reply_to_verkey = reply_to_verkey
self.target_list = target_list

async def create_outbound(
self,
Expand Down Expand Up @@ -133,7 +136,7 @@ async def send_reply(
reply_session_id=self.reply_session_id,
reply_to_verkey=self.reply_to_verkey,
target=target,
target_list=target_list,
target_list=target_list or self.target_list,
)
if isinstance(message, BaseMessage):
msg_type = message._message_type
Expand Down
Empty file.
Empty file.
10 changes: 10 additions & 0 deletions acapy_agent/protocols_v2/basicmessage/definition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Version definitions for this protocol."""

versions = [
{
"major_version": 1,
"minimum_minor_version": 0,
"current_minor_version": 0,
"path": "v1_0",
}
]
Empty file.
46 changes: 46 additions & 0 deletions acapy_agent/protocols_v2/basicmessage/v1_0/message_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Message type identifiers for Trust Pings."""

#from ...didcomm_prefix import DIDCommPrefix
import logging
from ....messaging.v2_agent_message import V2AgentMessage
from ....connections.models.connection_target import ConnectionTarget
from didcomm_messaging import DIDCommMessaging, RoutingService

SPEC_URI = (
"https://didcomm.org/basicmessage/2.0/message"
)

# Message types
BASIC_MESSAGE = "https://didcomm.org/basicmessage/2.0/message"

PROTOCOL_PACKAGE = "acapy_agent.protocols_v2.basicmessage.v1_0"

class basic_message:
async def __call__(self, *args, **kwargs):
await self.handle(*args, **kwargs)
@staticmethod
async def handle(context, responder, payload):
logger = logging.getLogger(__name__)
their_did = context.message_receipt.sender_verkey.split('#')[0]
our_did = context.message_receipt.recipient_verkey.split('#')[0]
error_result = V2AgentMessage(
message={
"type": BASIC_MESSAGE,
"body": {
"content": "Hello from acapy",
},
"to": [their_did],
"from": our_did,
"lang": "en",
}
)
await responder.send_reply(error_result)


HANDLERS = {
BASIC_MESSAGE: f"{PROTOCOL_PACKAGE}.message_types.basic_message",
}.items()

MESSAGE_TYPES = {
BASIC_MESSAGE: f"{PROTOCOL_PACKAGE}.message_types.basic_message",
}
Loading
Loading