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

Out-of-band credential offer invitation not recognized correctly by the receiving sub-wallet (multi-tenant ACA-Py)

Open nb-pratik-bhimani opened this issue 4 weeks ago • 1 comments

Summary

When Agent A creates an issue-credential-v2 offer and embeds it inside an Out-of-Band (OOB) invitation (connectionless), Agent B receives the OOB URL but ACA-Py does not correctly identify it as a credential offer.

The formats, or requests~attach information is not parsed correctly on the receiving agent, causing incorrect request-type detection.


Environment

  • ACA-Py version: 1.3.0
  • Multi-tenant mode: Enabled
  • Wallets involved: Sub-wallet A → sub-wallet B (Same multi-tenant Agent, different 2 wallets)
  • Message protocol: AIP 2.0 (Issue-Credential-v2)
  • DID method used: did:peer:4
  • Transport: HTTP

Steps to Reproduce

1. Agent A creates an Issue-Credential-V2 offer

POST /issue-credential-2.0/create-offer

Payload sent:

{
    "comment": "comment",
    "credential_preview": {
        "attributes": [
            {
                "name": "first_name",
                "value": "John"
            },
            {
                "name": "last_name",
                "value": "Doe"
            },
            {
                "name": "dob_dateint",
                "value": "1990-01-01"
            }
        ]
    },
    "filter": {
        "indy": {
            "cred_def_id": "2w1Eux9fobtPbXAbPJpjip:3:CL:3012385:Id_card",
            "issuer_did": "2w1Eux9fobtPbXAbPJpjip",
            "schema_issuer_did": "2w1Eux9fobtPbXAbPJpjip",
            "schema_name": "Id_card",
            "schema_version": "1.0"
        }
    },
    "auto_issue": true
}

2. Agent A wraps the offer into an OOB invitation

POST /out-of-band/create-invitation

Payload sent:

{
    "accept": [
        "AIP2_0"
    ],
    "handshake_protocols": [
        "https://didcomm.org/didexchange/1.0"
    ],
    "attachments": [
        {
            "id": "65f56f23-083a-4ba6-83e6-40dba01dd993",
            "type": "credential-offer"
        }
    ],
    "goal_code": "issue-vc",
    "goal": "issue a credential",
    "use_did_method": "did:peer:4"
}

This produces the OOB URL.

3. Agent B receives the OOB invitation URL

Receiver extracts:

{
    "@type": "https://didcomm.org/out-of-band/1.1/invitation",
    "@id": "34d31174-e7d2-40f3-86c7-2c85d03d4ce7",
    "label": "tenant agent",
    "handshake_protocols": [
        "https://didcomm.org/didexchange/1.0"
    ],
    "accept": [
        "AIP2_0"
    ],
    "requests~attach": [
        {
            "@id": "request-0",
            "mime-type": "application/json",
            "data": {
                "json": {
                    "@type": "https://didcomm.org/issue-credential/2.0/offer-credential",
                    "@id": "8c426c96-7e6c-4c7c-9839-5392d21e293a",
                    "~thread": {
                        "pthid": "34d31174-e7d2-40f3-86c7-2c85d03d4ce7"
                    },
                    "comment": "comment",
                    "credential_preview": {
                        "@type": "https://didcomm.org/issue-credential/2.0/credential-preview",
                        "attributes": [
                            {
                                "name": "first_name",
                                "value": "John"
                            },
                            {
                                "name": "last_name",
                                "value": "Doe"
                            },
                            {
                                "name": "dob_dateint",
                                "value": "1990-01-01"
                            }
                        ]
                    },
                    "formats": [
                        {
                            "attach_id": "indy",
                            "format": "hlindy/[email protected]"
                        }
                    ],
                    "offers~attach": [
                        {
                            "@id": "indy",
                            "mime-type": "application/json",
                            "data": {
                                "base64": " ... Base64 ..."
                            }
                        }
                    ]
                }
            }
        }
    ],
    "services": [
        "did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj:zUU97Vo6H3YqMGZRx6Z7MD3fmnzFX1wUCBfnoGBouTWLBzKL6HrhSMi7mNNiyuAJuhRaPCizPjpyuL7kycFSckZWVWu4bW2kQpk3apMmvU9JYXoDNytacTHTXtJUzegEv5Rh9PmFmPwfPgzv8igx3yhyj3DkVEf974Nawgi1HAtXhZytdzpdhcikq6syS8ieGUE6d7n7zaUMSS1fh5CejBrpYCqGMyzu8qEjkiP6SKyWUG7ZtHrLYffxgaw5kQrjRQcMy114xo5SbKXb3KPKkEUcU2zZfrduqeNtkDFd6TyhERtHdHqvSCWPLHRFpD4gTbzbUqzm31AqajRipnNP8coQFrPMQ3SCtTMJs99pjjLAYWA5qBEduiNpMQjnF3fef7cKKeS9m7gBZoFdAsYVe6chmBaYJ5w5RsCGrGPKcMzQt1VZDc9C2zfFAQeLEikWGMGjDegGaAmfhvvomun39qiyEMxeNHoy8UMiH3xDMKjrHoiQWteZE2AwnegJEJWje8NZPFV7wmdNZJr4SWpJG2b9ZM6DVJfHrKsS93dvvdV8kYy1WeAr2Zb8YEZjaTqQabNYv9dyT1s3h3pFjLL69dqQ3v4uwi"
    ]
}

4. Agent B calls

POST /out-of-band/receive-invitation

Expected Result

Agent B should classify this as:

  • Credential Offer
  • Goal code → issue-vc
  • formats → hlindy/[email protected]
  • Type → Issue-Credential-V2 flow

Actual Result

  • Receiver cannot determine the request type reliably.
  • formats not consistently passed into the final OobRecord.
  • ACA-Py returns incomplete OobRecord for invitations that include credential offers.

Actual Logs

Output from:

POST /out-of-band/receive-invitation
Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 444, in resolve_didcomm_services
    doc_dict: dict = await resolver.resolve(self._profile, did, service_accept)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 87, in resolve
    _, doc = await self._resolve(profile, did, service_accept, timeout=timeout)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 76, in _resolve
    raise DIDNotFound(f"DID {did} could not be resolved")
acapy_agent.resolver.base.DIDNotFound: DID did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj could not be resolved

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 314, in __step_run_and_handle_result
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 222, in upgrade_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp_apispec/middlewares.py", line 51, in validation_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/decorators/auth.py", line 84, in tenant_auth
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/routes.py", line 372, in invitation_receive
    result = await oob_mgr.receive_invitation(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 755, in receive_invitation
    oob_record = await self._perform_handshake(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 1091, in _perform_handshake
    conn_record = await didx_mgr.receive_invitation(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/didexchange/v1_0/manager.py", line 159, in receive_invitation
    targets = await self.resolve_connection_targets(conn_rec.their_public_did)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 541, in resolve_connection_targets
    doc, didcomm_services = await self.resolve_didcomm_services(did)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 447, in resolve_didcomm_services
    raise BaseConnectionManagerError("Failed to resolve DID services") from error
acapy_agent.connections.base_manager.BaseConnectionManagerError: Failed to resolve DID services
2025-12-01 11:50:30,806 acapy_agent.admin.server ERROR Handler error with exception:
Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 444, in resolve_didcomm_services
    doc_dict: dict = await resolver.resolve(self._profile, did, service_accept)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 87, in resolve
    _, doc = await self._resolve(profile, did, service_accept, timeout=timeout)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 76, in _resolve
    raise DIDNotFound(f"DID {did} could not be resolved")
acapy_agent.resolver.base.DIDNotFound: DID did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj could not be resolved

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 146, in ready_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 238, in debug_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 360, in setup_context
    return await task
           ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 289, in __await__
    yield self  # This tells Task to wait for completion.
    ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 385, in __wakeup
    future.result()
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 202, in result
    raise self._exception.with_traceback(self._exception_tb)
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 314, in __step_run_and_handle_result
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 222, in upgrade_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp_apispec/middlewares.py", line 51, in validation_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/decorators/auth.py", line 84, in tenant_auth
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/routes.py", line 372, in invitation_receive
    result = await oob_mgr.receive_invitation(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 755, in receive_invitation
    oob_record = await self._perform_handshake(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 1091, in _perform_handshake
    conn_record = await didx_mgr.receive_invitation(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/didexchange/v1_0/manager.py", line 159, in receive_invitation
    targets = await self.resolve_connection_targets(conn_rec.their_public_did)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 541, in resolve_connection_targets
    doc, didcomm_services = await self.resolve_didcomm_services(did)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 447, in resolve_didcomm_services
    raise BaseConnectionManagerError("Failed to resolve DID services") from error
acapy_agent.connections.base_manager.BaseConnectionManagerError: Failed to resolve DID services
2025-12-01 11:50:30,808 aiohttp.server ERROR Error handling request from 192.168.1.13
Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 444, in resolve_didcomm_services
    doc_dict: dict = await resolver.resolve(self._profile, did, service_accept)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 87, in resolve
    _, doc = await self._resolve(profile, did, service_accept, timeout=timeout)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/resolver/did_resolver.py", line 76, in _resolve
    raise DIDNotFound(f"DID {did} could not be resolved")
acapy_agent.resolver.base.DIDNotFound: DID did:peer:4zQmZ8MpHxdbJWvPhuqz4bsVDLJQeRNo7SFECGgiVggQ2tVj could not be resolved

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp/web_protocol.py", line 510, in _handle_request
    resp = await request_handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp/web_app.py", line 569, in _handle
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp/web_middlewares.py", line 117, in impl
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 146, in ready_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 238, in debug_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 360, in setup_context
    return await task
           ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 289, in __await__
    yield self  # This tells Task to wait for completion.
    ^^^^^^^^^^
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 385, in __wakeup
    future.result()
  File "/usr/local/lib/python3.12/asyncio/futures.py", line 202, in result
    raise self._exception.with_traceback(self._exception_tb)
  File "/usr/local/lib/python3.12/asyncio/tasks.py", line 314, in __step_run_and_handle_result
    result = coro.send(None)
             ^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/server.py", line 222, in upgrade_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/aiohttp_apispec/middlewares.py", line 51, in validation_middleware
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/admin/decorators/auth.py", line 84, in tenant_auth
    return await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/routes.py", line 372, in invitation_receive
    result = await oob_mgr.receive_invitation(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 755, in receive_invitation
    oob_record = await self._perform_handshake(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/out_of_band/v1_0/manager.py", line 1091, in _perform_handshake
    conn_record = await didx_mgr.receive_invitation(
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/protocols/didexchange/v1_0/manager.py", line 159, in receive_invitation
    targets = await self.resolve_connection_targets(conn_rec.their_public_did)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 541, in resolve_connection_targets
    doc, didcomm_services = await self.resolve_didcomm_services(did)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/aries/.local/lib/python3.12/site-packages/acapy_agent/connections/base_manager.py", line 447, in resolve_didcomm_services
    raise BaseConnectionManagerError("Failed to resolve DID services") from error
acapy_agent.connections.base_manager.BaseConnectionManagerError: Failed to resolve DID services


Problem Summary

The OOB invitation contains a valid Issue-Credential-V2 offer, but ACA-Py’s internal handling of OOB 1.1 → credential offer mapping appears inconsistent:

  • formats inside requests~attach are not reflected in the returned OOB record.
  • Makes it impossible to reliably detect credential-offer vs proof-request on the receiving agent.

This behavior breaks multi-tenant workflows where OOB invitations are used to pass issue-credential-v2 offers.


Possible Causes

  1. requests~attachcredJson not parsed fully
  2. Issue-Credential-V2 “offer-credential” is not mapped to ACA-Py goal codes internally

Request

Please confirm whether:

  • This is an intended ACA-Py behavior OR
  • A bug in OOB v1.1 behavior when wrapping Issue-Credential-v2 offer messages.

If this is a bug, guidance or a fix would be appreciated.


Additional Context

This use-case is sub-wallet to sub-wallet (same agent instance) .

nb-pratik-bhimani avatar Dec 01 '25 13:12 nb-pratik-bhimani

This seems like a bug in OOB v1.1 behavior.

For some reason it isn't able to resolve the did:peer:4. Could you try using https://didcomm.org/didexchange/1.1 handshake_protocol and see if the same error happens? It uses did:peer:4 by default.

We will probably need to debug the protocol on the holder side to determine why that did isn't resolvable. I don't have any answers for you. It would likely work if you create the connection first and then issue the credential instead of adding as an attachment.

There has been some other issues opened up about the invitation attachment which haven't been fixed. This could also just be a acapy holder issue and the invitation may work correctly when consumed by a credo-ts agent.

jamshale avatar Dec 01 '25 18:12 jamshale