aries-cloudagent-python icon indicating copy to clipboard operation
aries-cloudagent-python copied to clipboard

Confusing their_role value in ACA-Py connection request – shows inviter for request-sender

Open nb-vivek-bodar opened this issue 2 months ago • 3 comments

  • In the current ACA-Py implementation, there's potential confusion around the their_role field across all connection states — not just during the request phase.

Definitions (as understood from documentation):

  1. Inviter → The party that sends the invitation.
  2. Invitee → The party that receives the invitation and sends a connection request.

Observation:

  • When a invitation request is sent (by the Inviter), the their_role is returned as inviter—this seems counterintuitive, especially for client applications trying to determine who initiated the connection.

Steps to Reproduce

  1. Holder (Inviter) creates an invitation using ACA-Py:
  • POST /didexchange/create-invitation
  • Response includes the invitation details:
{
  "state": "request",
  "created_at": "2025-09-29T14:02:21.243697Z",
  "updated_at": "2025-09-29T14:02:21.252565Z",
  "connection_id": "2cc619e9-89fb-4f79-8595-e1fba0d7e995",
  "my_did": "JE2seYAKJVkuGYvdurbJwA",
  "their_did": "did:sov:hMdL27Sh3voQzhr1vvGJ3",
  "their_role": "inviter",
  "connection_protocol": "didexchange/1.0",
  "rfc23_state": "request-sent",
  "request_id": "2649c13e-4d55-4e8e-a5c8-edcb4b088927",
  "accept": "manual",
  "invitation_mode": "once",
  "alias": "John Holder",
  "their_public_did": "did:sov:hMdL27Sh3voQzhr1vvGJ3"
}
  1. Issuer (Invitee) receives the invitation request:
  • GET /connections
  • Response includes the invitation details:
{
  "state": "request",
  "created_at": "2025-09-29T14:02:21.391297Z",
  "updated_at": "2025-09-29T14:02:21.391297Z",
  "connection_id": "17daa1b0-3ab2-4528-81e4-aff89a932a0f",
  "their_did": "JE2seYAKJVkuGYvdurbJwA",
  "their_label": "Holder Agent",
  "their_role": "invitee",
  "connection_protocol": "didexchange/1.0",
  "rfc23_state": "request-received",
  "invitation_key": "Nzip9urmchNE2JGsjnp66ZRNYNSDibimrAMZvCvk1sK",
  "request_id": "2649c13e-4d55-4e8e-a5c8-edcb4b088927",
  "accept": "manual",
  "invitation_mode": "once"
} 

Environment

  • ACA-Py version: 1.3.0
  • Protocol: didexchange/1.0
  • Transport: HTTP / Admin API

nb-vivek-bodar avatar Oct 03 '25 11:10 nb-vivek-bodar

I agree that it's confusing. Might be a mistake, but I'm not sure.

What's particularly confusing to me is that the DID Exchange protocol roles should actually be "requester" or "responder".

And the ConnRecord class caters for the distinct roles depending on the protocol:

class ConnRecord(BaseRecord):
    """Represents a single pairwise connection."""
...

    class Role(Enum):
        """RFC 160 (inviter, invitee) = RFC 23 (responder, requester)."""

        REQUESTER = ("invitee", "requester")  # == RFC 23 initiator, RFC 434 receiver
        RESPONDER = ("inviter", "responder")  # == RFC 160 initiator(!), RFC 434 sender

        @property
        def rfc160(self):  # -> Literal['invitee', 'inviter']
            """Return RFC 160 (connection protocol) nomenclature."""
            return self.value[0]

        @property
        def rfc23(self):  # -> Literal['requester', 'responder']
            """Return RFC 23 (DID exchange protocol) nomenclature."""
            return self.value[1]

rfc23 is used throughout the DIDXManager. e.g.: The POST /didexchange/create-request endpoint calls DIDXManager.create_request_implicit, which creates the connection record:

        conn_rec = ConnRecord(
            ..., their_role=ConnRecord.Role.RESPONDER.rfc23, ...
        )

That means, according to the code, their_role should be "responder" (rfc23 version of "inviter"). But it must be overwritten / converted somewhere, and after digging for a bit I can't see where that change is happening...

That's kind of secondary to the question whether it should be invitee/requester. But just wanted to share that as an additional uncertainty. Not sure why/how it's switching from rfc23 to rfc160, somewhere between the record being created and returned.

ff137 avatar Oct 03 '25 15:10 ff137

Definitely sounds like a bug. The RFC23 / RFC160 issue is bad enough -- especially since RFC23 is the NEW approach, and RFC160 is the deprecated approach. Spec'ing while implementing is painful at times...

It would be good to see where the value is changed, as @ff137 mentions.

swcurran avatar Oct 03 '25 16:10 swcurran

@swcurran I found the rfc23/rfc160 mismatch is in the ConnRecord init constructor, where it overrides their_role to use the rfc160 version. Change was last applied here, where stored values use the legacy values:

  • https://github.com/openwallet-foundation/acapy/pull/790

That was 5 years ago 👀 and it was legacy then... so it's probably good to make a change now?


The issue with changing how roles are stored, is that many methods retrieve connection records based on their_role, e.g.:

class ConnRecord(BaseRecord):
    """Represents a single pairwise connection."""
...

    @classmethod
    async def retrieve_by_request_id(
        cls, session: ProfileSession, request_id: str, their_role: Optional[str] = None
    ) -> "ConnRecord":
        """Retrieve a connection record from our previous request ID.

        Args:
            session: The active profile session
            request_id: The ID of the originating connection request
            their_role: Filter by their role
        """
        tag_filter = {"request_id": request_id}
        if their_role:
            tag_filter["their_role"] = their_role
        return await cls.retrieve_by_tag_filter(session, tag_filter)

Changing inviter -> requester (same role, different protocol) in how records are stored is not too bad. Because Role is actually an Enum, and it's being stored and read as a string. This would be better if Role is always treated as an Enum. So the retrieve methods fetch by Role, not str -- and the string value that finally gets displayed depends on the connection_protocol. That way the stored record values don't need to change.

However, changing invitee -> inviter/requester is of course a much bigger change, because it would entail migrating old records to also follow the correct pattern. Or just having the caveat that ConnRecord "their_role" metadata is reversed, if it was created before a certain version. As far as I can tell it does seem to be reversed. (I always understood it as "the issuer giving their public did is like initiating the invite", but that was just a guess, trying to rationalise the observation! Not based on any docs.)

I can help with the former change, using Enums instead of strings for the role. But the latter one I'll leave for others to discuss and decide upon.

ff137 avatar Oct 06 '25 09:10 ff137