From c39697eb4d5fb6b8cb88a7753f1dc608537e2bf6 Mon Sep 17 00:00:00 2001 From: Vijay Soni Date: Tue, 23 Dec 2025 16:44:29 -0600 Subject: [PATCH 01/13] fixed duplicate present-proof v2 webhook Signed-off-by: Vijay Soni --- .../v2_0/models/pres_exchange.py | 4 ++ .../v2_0/models/tests/test_record.py | 71 ++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py b/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py index 3774dd70e8..46139d546e 100644 --- a/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py +++ b/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py @@ -206,6 +206,10 @@ async def emit_event(self, session: ProfileSession, payload: Optional[Any] = Non if not session.profile.settings.get("debug.webhooks"): payload = V20PresExRecordWebhook(**payload) payload = payload.__dict__ + # BUG #3802: remove legacy fields when by_format is present + elif payload.get("by_format"): + for key in ("pres_proposal", "pres_request", "pres"): + payload.pop(key, None) await session.emit_event(topic, payload) diff --git a/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py b/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py index 9890ba6cb3..74d78ec40d 100644 --- a/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py +++ b/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py @@ -4,9 +4,11 @@ from ......messaging.models.base_record import BaseExchangeRecord, BaseExchangeSchema from ......tests import mock from ......utils.testing import create_test_profile -from ...message_types import ATTACHMENT_FORMAT, PRES_20_PROPOSAL +from ...message_types import ATTACHMENT_FORMAT, PRES_20, PRES_20_PROPOSAL, PRES_20_REQUEST +from ...messages.pres import V20Pres from ...messages.pres_format import V20PresFormat from ...messages.pres_proposal import V20PresProposal +from ...messages.pres_request import V20PresRequest from .. import pres_exchange as test_module from ..pres_exchange import V20PresExRecord @@ -52,6 +54,16 @@ } }, } +INDY_PROOF = { + "proof": {"proofs": []}, + "requested_proof": { + "revealed_attrs": {}, + "self_attested_attrs": {}, + "unrevealed_attrs": {}, + "predicates": {}, + }, + "identifiers": [], +} class BasexRecordImpl(BaseExchangeRecord): @@ -134,3 +146,60 @@ async def test_save_error_state(self): mock_save.side_effect = test_module.StorageError() await record.save_error_state(session, reason="testing") mock_log_exc.assert_called_once() + + # BUG #3802: ensure webhook payloads omit legacy pres_* fields when by_format exists + async def test_emit_event_strips_legacy_pres_fields(self): + settings = { + "wallet.type": "askar", + "auto_provision": True, + "wallet.key": "5BngFuBpS4wjFfVFCtPqoix3ZXG2XR8XJ7qosUzMak7R", + "wallet.key_derivation_method": "RAW", + "debug.webhooks": True, + } + self.profile = await create_test_profile(settings=settings) + pres_request = V20PresRequest( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_REQUEST][ + V20PresFormat.Format.INDY.api + ], + ) + ], + request_presentations_attach=[ + AttachDecorator.data_base64(mapping=INDY_PROOF_REQ, ident="indy") + ], + ) + pres = V20Pres( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20][ + V20PresFormat.Format.INDY.api + ], + ) + ], + presentations_attach=[ + AttachDecorator.data_base64(mapping=INDY_PROOF, ident="indy") + ], + ) + record = V20PresExRecord( + pres_ex_id="pxid", + thread_id="thid", + connection_id="conn_id", + initiator="init", + role="role", + state=V20PresExRecord.STATE_PRESENTATION_RECEIVED, + pres_request=pres_request, + pres=pres, + ) + + async with self.profile.session() as session: + session.emit_event = mock.CoroutineMock() + await record.emit_event(session) + + payload = session.emit_event.call_args.args[1] + assert "by_format" in payload + assert "pres_request" not in payload + assert "pres" not in payload + assert "pres_proposal" not in payload From 67c1a26a9afd7bc0e2cd2fb1fe32817b267d5c96 Mon Sep 17 00:00:00 2001 From: Vijay Soni Date: Sat, 3 Jan 2026 12:29:53 -0600 Subject: [PATCH 02/13] fixed sonar issues Signed-off-by: Vijay Soni --- .../example.py | 17 ++++++++-- .../example.py | 17 ++++++++-- .../presenting_revoked_credential/example.py | 17 ++++++++-- scenarios/examples/util.py | 33 ++++++++++++++----- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/scenarios/examples/did_indy_issuance_and_revocation/example.py b/scenarios/examples/did_indy_issuance_and_revocation/example.py index 77b186e41c..ced335c680 100644 --- a/scenarios/examples/did_indy_issuance_and_revocation/example.py +++ b/scenarios/examples/did_indy_issuance_and_revocation/example.py @@ -27,14 +27,27 @@ BOB = getenv("BOB", "http://bob:3001") +def _presentation_request_payload(presentation: V20PresExRecord): + if presentation.by_format and presentation.by_format.pres_request: + return presentation.by_format.pres_request + request = presentation.pres_request + if not request: + return None + if isinstance(request, dict): + return request + if hasattr(request, "model_dump"): + return request.model_dump(by_alias=True) + return request.dict(by_alias=True) + + def summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" - request = presentation.pres_request + request = _presentation_request_payload(presentation) return "Summary: " + json.dumps( { "state": presentation.state, "verified": presentation.verified, - "presentation_request": request.dict(by_alias=True) if request else None, + "presentation_request": request, }, indent=2, sort_keys=True, diff --git a/scenarios/examples/kanon_issuance_and_presentation/example.py b/scenarios/examples/kanon_issuance_and_presentation/example.py index 5ec12591d1..7cff659567 100644 --- a/scenarios/examples/kanon_issuance_and_presentation/example.py +++ b/scenarios/examples/kanon_issuance_and_presentation/example.py @@ -29,14 +29,27 @@ BOB = getenv("BOB", "http://bob:3001") +def _presentation_request_payload(presentation: V20PresExRecord): + if presentation.by_format and presentation.by_format.pres_request: + return presentation.by_format.pres_request + request = presentation.pres_request + if not request: + return None + if isinstance(request, dict): + return request + if hasattr(request, "model_dump"): + return request.model_dump(by_alias=True) + return request.dict(by_alias=True) + + def summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" - request = presentation.pres_request + request = _presentation_request_payload(presentation) return "Summary: " + json.dumps( { "state": presentation.state, "verified": presentation.verified, - "presentation_request": request.dict(by_alias=True) if request else None, + "presentation_request": request, }, indent=2, sort_keys=True, diff --git a/scenarios/examples/presenting_revoked_credential/example.py b/scenarios/examples/presenting_revoked_credential/example.py index 955bf6a07a..7efeabda6e 100644 --- a/scenarios/examples/presenting_revoked_credential/example.py +++ b/scenarios/examples/presenting_revoked_credential/example.py @@ -27,14 +27,27 @@ BOB = getenv("BOB", "http://bob:3001") +def _presentation_request_payload(presentation: V20PresExRecord): + if presentation.by_format and presentation.by_format.pres_request: + return presentation.by_format.pres_request + request = presentation.pres_request + if not request: + return None + if isinstance(request, dict): + return request + if hasattr(request, "model_dump"): + return request.model_dump(by_alias=True) + return request.dict(by_alias=True) + + def summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" - request = presentation.pres_request + request = _presentation_request_payload(presentation) return "Summary: " + json.dumps( { "state": presentation.state, "verified": presentation.verified, - "presentation_request": request.dict(by_alias=True) if request else None, + "presentation_request": request, }, indent=2, sort_keys=True, diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index 5ecbe72c7b..f1b93ebbb1 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -56,16 +56,29 @@ def get_wallet_name(agent_command: List) -> str: # anoncreds utilities: +def _presentation_request_payload( + presentation: V20PresExRecord, +) -> Optional[Dict[str, Any]]: + if presentation.by_format and presentation.by_format.pres_request: + return presentation.by_format.pres_request + request = presentation.pres_request + if not request: + return None + if isinstance(request, dict): + return request + if hasattr(request, "model_dump"): + return request.model_dump(by_alias=True) + return request.dict(by_alias=True) + + def anoncreds_presentation_summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" - request = presentation.pres_request + request = _presentation_request_payload(presentation) return "Summary: " + json.dumps( { "state": presentation.state, "verified": presentation.verified, - "presentation_request": ( - request.model_dump(by_alias=True) if request else None - ), + "presentation_request": request, }, indent=2, sort_keys=True, @@ -413,7 +426,6 @@ async def anoncreds_present_proof_v2( connection_id=holder_connection_id, state="request-received", ) - assert holder_pres_ex.pres_request holder_pres_ex_id = holder_pres_ex.pres_ex_id relevant_creds = await holder.get( @@ -429,10 +441,13 @@ async def anoncreds_present_proof_v2( if cred.cred_info._extra.get("cred_rev_id") == cred_rev_id ] - assert holder_pres_ex.by_format.pres_request - proof_request = holder_pres_ex.by_format.pres_request.get( - "anoncreds" - ) or holder_pres_ex.by_format.pres_request.get("indy") + request_payload = _presentation_request_payload(holder_pres_ex) + assert request_payload + if "anoncreds" in request_payload or "indy" in request_payload: + proof_request = request_payload.get("anoncreds") or request_payload.get("indy") + else: + proof_request = request_payload + assert proof_request pres_spec = auto_select_credentials_for_presentation_request( proof_request, relevant_creds ) From 50ec11c90d7d33d5c847071afb8d3f6fdaa310cf Mon Sep 17 00:00:00 2001 From: Vijay Soni Date: Sat, 3 Jan 2026 12:30:21 -0600 Subject: [PATCH 03/13] fixed linting err Signed-off-by: Vijay Soni --- .../protocols/present_proof/v2_0/models/tests/test_record.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py b/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py index 74d78ec40d..701fba8e0c 100644 --- a/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py +++ b/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py @@ -174,9 +174,7 @@ async def test_emit_event_strips_legacy_pres_fields(self): formats=[ V20PresFormat( attach_id="indy", - format_=ATTACHMENT_FORMAT[PRES_20][ - V20PresFormat.Format.INDY.api - ], + format_=ATTACHMENT_FORMAT[PRES_20][V20PresFormat.Format.INDY.api], ) ], presentations_attach=[ From ad718db411835b04aaec0cb8fde7185e057ab7af Mon Sep 17 00:00:00 2001 From: Vijay Soni Date: Sat, 3 Jan 2026 13:02:39 -0600 Subject: [PATCH 04/13] fixed test errors Signed-off-by: Vijay Soni --- .../example.py | 2 +- scenarios/examples/multitenancy/example.py | 2 +- .../presenting_revoked_credential/example.py | 2 +- scenarios/examples/simple_restart/example.py | 3 +- scenarios/examples/util.py | 109 ++++++++++++++++++ 5 files changed, 113 insertions(+), 5 deletions(-) diff --git a/scenarios/examples/did_indy_issuance_and_revocation/example.py b/scenarios/examples/did_indy_issuance_and_revocation/example.py index ced335c680..2de5d4a201 100644 --- a/scenarios/examples/did_indy_issuance_and_revocation/example.py +++ b/scenarios/examples/did_indy_issuance_and_revocation/example.py @@ -18,10 +18,10 @@ didexchange, indy_anoncred_credential_artifacts, indy_issue_credential_v2, - indy_present_proof_v2, params, ) from aiohttp import ClientSession +from examples.util import indy_present_proof_v2 ALICE = getenv("ALICE", "http://alice:3001") BOB = getenv("BOB", "http://bob:3001") diff --git a/scenarios/examples/multitenancy/example.py b/scenarios/examples/multitenancy/example.py index 4844ec85e5..9f986974fa 100644 --- a/scenarios/examples/multitenancy/example.py +++ b/scenarios/examples/multitenancy/example.py @@ -14,10 +14,10 @@ didexchange, indy_anoncred_credential_artifacts, indy_issue_credential_v2, - indy_present_proof_v2, params, ) from aiohttp import ClientSession +from examples.util import indy_present_proof_v2 AGENCY = getenv("AGENCY", "http://agency:3001") diff --git a/scenarios/examples/presenting_revoked_credential/example.py b/scenarios/examples/presenting_revoked_credential/example.py index 7efeabda6e..97d8ef54f0 100644 --- a/scenarios/examples/presenting_revoked_credential/example.py +++ b/scenarios/examples/presenting_revoked_credential/example.py @@ -18,10 +18,10 @@ didexchange, indy_anoncred_credential_artifacts, indy_issue_credential_v2, - indy_present_proof_v2, params, ) from aiohttp import ClientSession +from examples.util import indy_present_proof_v2 ALICE = getenv("ALICE", "http://alice:3001") BOB = getenv("BOB", "http://bob:3001") diff --git a/scenarios/examples/simple_restart/example.py b/scenarios/examples/simple_restart/example.py index 76b4502704..0c0fc2a856 100644 --- a/scenarios/examples/simple_restart/example.py +++ b/scenarios/examples/simple_restart/example.py @@ -14,9 +14,8 @@ indy_anoncred_credential_artifacts, indy_anoncred_onboard, indy_issue_credential_v2, - indy_present_proof_v2, ) -from examples.util import wait_until_healthy +from examples.util import indy_present_proof_v2, wait_until_healthy import docker diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index f1b93ebbb1..37fb7275f4 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -1,3 +1,5 @@ +"""Scenario helpers for ACA-Py examples.""" + import json import time from dataclasses import dataclass @@ -41,6 +43,7 @@ def wait_until_healthy(client, container_id: str, attempts: int = 350, is_health def update_wallet_type(agent_command: List, wallet_type: str) -> str: + """Update the wallet type argument in a CLI command list.""" for i in range(len(agent_command) - 1): if agent_command[i] == "--wallet-type": agent_command[i + 1] = wallet_type @@ -49,6 +52,7 @@ def update_wallet_type(agent_command: List, wallet_type: str) -> str: def get_wallet_name(agent_command: List) -> str: + """Return the wallet name argument from a CLI command list.""" for i in range(len(agent_command) - 1): if agent_command[i] == "--wallet-name": return agent_command[i + 1] @@ -204,6 +208,111 @@ def auto_select_credentials_for_presentation_request( ) +async def indy_present_proof_v2( + holder: Controller, + verifier: Controller, + holder_connection_id: str, + verifier_connection_id: str, + *, + name: Optional[str] = None, + version: Optional[str] = None, + comment: Optional[str] = None, + requested_attributes: Optional[List[Mapping[str, Any]]] = None, + requested_predicates: Optional[List[Mapping[str, Any]]] = None, + non_revoked: Optional[Mapping[str, int]] = None, + cred_rev_id: Optional[str] = None, +): + """Present a credential using present proof v2 (indy).""" + attrs = { + "name": name or "proof", + "version": version or "0.1.0", + "nonce": str(randbelow(10**10)), + "requested_attributes": { + str(uuid4()): attr for attr in requested_attributes or [] + }, + "requested_predicates": { + str(uuid4()): pred for pred in requested_predicates or [] + }, + "non_revoked": (non_revoked if non_revoked else None), + } + + verifier_pres_ex = await verifier.post( + "/present-proof-2.0/send-request", + json={ + "auto_verify": False, + "comment": comment or "Presentation request from minimal", + "connection_id": verifier_connection_id, + "presentation_request": {"indy": attrs}, + "trace": False, + }, + response=V20PresExRecord, + ) + verifier_pres_ex_id = verifier_pres_ex.pres_ex_id + + holder_pres_ex = await holder.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + connection_id=holder_connection_id, + state="request-received", + ) + holder_pres_ex_id = holder_pres_ex.pres_ex_id + + relevant_creds = await holder.get( + f"/present-proof-2.0/records/{holder_pres_ex_id}/credentials", + response=List[CredPrecis], + ) + + if cred_rev_id: + relevant_creds = [ + cred + for cred in relevant_creds + if cred.cred_info._extra.get("cred_rev_id") == cred_rev_id + ] + + request_payload = _presentation_request_payload(holder_pres_ex) + assert request_payload + if "anoncreds" in request_payload or "indy" in request_payload: + proof_request = request_payload.get("indy") or request_payload.get("anoncreds") + else: + proof_request = request_payload + assert proof_request + pres_spec = auto_select_credentials_for_presentation_request( + proof_request, relevant_creds + ) + await holder.post( + f"/present-proof-2.0/records/{holder_pres_ex_id}/send-presentation", + json={"indy": pres_spec.serialize()}, + response=V20PresExRecord, + ) + + await verifier.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=verifier_pres_ex_id, + state="presentation-received", + ) + await verifier.post( + f"/present-proof-2.0/records/{verifier_pres_ex_id}/verify-presentation", + json={}, + response=V20PresExRecord, + ) + verifier_pres_ex = await verifier.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=verifier_pres_ex_id, + state="done", + ) + + holder_pres_ex = await holder.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=holder_pres_ex_id, + state="done", + ) + + return holder_pres_ex, verifier_pres_ex + + async def anoncreds_issue_credential_v2( issuer: Controller, holder: Controller, From d3fa5a13a4e424c1452a1b9484a20b8c080a1c25 Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Wed, 18 Feb 2026 22:52:21 -0600 Subject: [PATCH 05/13] fixed JSON-LD regression test failure Signed-off-by: Vijay Kumar Soni --- scenarios/examples/json_ld/example.py | 10 ++-- scenarios/examples/util.py | 76 +++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/scenarios/examples/json_ld/example.py b/scenarios/examples/json_ld/example.py index 1629335fc0..4b5e49f95d 100644 --- a/scenarios/examples/json_ld/example.py +++ b/scenarios/examples/json_ld/example.py @@ -15,10 +15,10 @@ from acapy_controller.protocols import ( didexchange, jsonld_issue_credential, - jsonld_present_proof, params, ) from aiohttp import ClientSession +from examples.util import jsonld_present_proof_v2 ALICE = getenv("ALICE", "http://alice:3001") BOB = getenv("BOB", "http://bob:3001") @@ -159,7 +159,7 @@ async def main(): pause_for_input() with section("Present example ED25519 credential"): - alice_pres_ex, bob_pres_ex = await jsonld_present_proof( + alice_pres_ex, bob_pres_ex = await jsonld_present_proof_v2( alice, bob, alice_conn.connection_id, @@ -241,7 +241,7 @@ async def main(): pause_for_input() with section("Present example P256 credential"): - alice_pres_ex, bob_pres_ex = await jsonld_present_proof( + alice_pres_ex, bob_pres_ex = await jsonld_present_proof_v2( alice, bob, alice_conn.connection_id, @@ -327,7 +327,7 @@ async def main(): pause_for_input() with section("Present ED25519 quick context credential"): - alice_pres_ex, bob_pres_ex = await jsonld_present_proof( + alice_pres_ex, bob_pres_ex = await jsonld_present_proof_v2( alice, bob, alice_conn.connection_id, @@ -405,7 +405,7 @@ async def main(): pause_for_input() with section("Present BBS+ Credential with SD"): - alice_pres_ex, bob_pres_ex = await jsonld_present_proof( + alice_pres_ex, bob_pres_ex = await jsonld_present_proof_v2( alice, bob, alice_conn.connection_id, diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index 37fb7275f4..136b84df54 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -313,6 +313,82 @@ async def indy_present_proof_v2( return holder_pres_ex, verifier_pres_ex +async def jsonld_present_proof_v2( + holder: Controller, + verifier: Controller, + holder_connection_id: str, + verifier_connection_id: str, + *, + presentation_definition: Mapping[str, Any], + domain: Optional[str] = None, + challenge: Optional[str] = None, + comment: Optional[str] = None, +): + """Present a credential using present proof v2 (DIF/JSON-LD).""" + dif_options: Dict[str, Any] = {"challenge": challenge or str(uuid4())} + if domain: + dif_options["domain"] = domain + + verifier_pres_ex = await verifier.post( + "/present-proof-2.0/send-request", + json={ + "auto_verify": False, + "comment": comment or "Presentation request from minimal", + "connection_id": verifier_connection_id, + "presentation_request": { + "dif": { + "presentation_definition": dict(presentation_definition), + "options": dif_options, + } + }, + "trace": False, + }, + response=V20PresExRecord, + ) + verifier_pres_ex_id = verifier_pres_ex.pres_ex_id + + holder_pres_ex = await holder.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + connection_id=holder_connection_id, + state="request-received", + ) + holder_pres_ex_id = holder_pres_ex.pres_ex_id + + await holder.post( + f"/present-proof-2.0/records/{holder_pres_ex_id}/send-presentation", + json={"dif": {}}, + response=V20PresExRecord, + ) + + await verifier.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=verifier_pres_ex_id, + state="presentation-received", + ) + await verifier.post( + f"/present-proof-2.0/records/{verifier_pres_ex_id}/verify-presentation", + json={}, + response=V20PresExRecord, + ) + verifier_pres_ex = await verifier.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=verifier_pres_ex_id, + state="done", + ) + + holder_pres_ex = await holder.event_with_values( + topic="present_proof_v2_0", + event_type=V20PresExRecord, + pres_ex_id=holder_pres_ex_id, + state="done", + ) + + return holder_pres_ex, verifier_pres_ex + + async def anoncreds_issue_credential_v2( issuer: Controller, holder: Controller, From 07e33eeda28cb80a63b46c7db960017d73964da9 Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Wed, 18 Feb 2026 23:20:52 -0600 Subject: [PATCH 06/13] Skipping BBS+ flow when not present Signed-off-by: Vijay Kumar Soni --- scenarios/examples/json_ld/example.py | 64 +++++++++++++++------------ 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/scenarios/examples/json_ld/example.py b/scenarios/examples/json_ld/example.py index 4b5e49f95d..b45b326baf 100644 --- a/scenarios/examples/json_ld/example.py +++ b/scenarios/examples/json_ld/example.py @@ -10,6 +10,7 @@ from uuid import uuid4 from acapy_controller import Controller +from acapy_controller.controller import ControllerError from acapy_controller.logging import logging_to_stdout, pause_for_input, section from acapy_controller.models import DIDResult, V20PresExRecord from acapy_controller.protocols import ( @@ -207,7 +208,7 @@ async def main(): domain="test-degree", ) with section("Presentation summary", character="-"): - print(presentation_summary(alice_pres_ex.into(V20PresExRecord))) + print(presentation_summary(alice_pres_ex)) pause_for_input() @@ -294,7 +295,7 @@ async def main(): domain="test-degree", ) with section("Presentation summary", character="-"): - print(presentation_summary(alice_pres_ex.into(V20PresExRecord))) + print(presentation_summary(alice_pres_ex)) pause_for_input() @@ -370,37 +371,44 @@ async def main(): domain="test-degree", ) with section("Presentation summary", character="-"): - print(presentation_summary(alice_pres_ex.into(V20PresExRecord))) + print(presentation_summary(alice_pres_ex)) pause_for_input() with section("Issue BBS+ Credential"): - issuer_cred_ex, holder_cred_ex = await jsonld_issue_credential( - alice, - bob, - alice_conn.connection_id, - bob_conn.connection_id, - credential={ - "@context": [ - "https://www.w3.org/2018/credentials/v1", - { - "ex": "https://example.com/examples#", - "Employment": "ex:Employment", - "dateHired": "ex:dateHired", - "clearance": "ex:clearance", + try: + issuer_cred_ex, holder_cred_ex = await jsonld_issue_credential( + alice, + bob, + alice_conn.connection_id, + bob_conn.connection_id, + credential={ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "ex": "https://example.com/examples#", + "Employment": "ex:Employment", + "dateHired": "ex:dateHired", + "clearance": "ex:clearance", + }, + ], + "type": ["VerifiableCredential", "Employment"], + "issuer": bls_alice_did, + "issuanceDate": str(date.today()), + "credentialSubject": { + "id": bls_bob_did, + "dateHired": str(date.today()), + "clearance": 1, }, - ], - "type": ["VerifiableCredential", "Employment"], - "issuer": bls_alice_did, - "issuanceDate": str(date.today()), - "credentialSubject": { - "id": bls_bob_did, - "dateHired": str(date.today()), - "clearance": 1, }, - }, - options={"proofType": "BbsBlsSignature2020"}, - ) + options={"proofType": "BbsBlsSignature2020"}, + ) + except ControllerError as err: + print( + "Skipping BBS+ flow due to runtime capability/error: " + f"{err}" + ) + return pause_for_input() @@ -447,7 +455,7 @@ async def main(): domain="building-access", ) with section("Presentation summary", character="-"): - print(presentation_summary(alice_pres_ex.into(V20PresExRecord))) + print(presentation_summary(alice_pres_ex)) if __name__ == "__main__": From 3d5e7a7d404c65fc0677157c54945a6d1216cd14 Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Thu, 19 Feb 2026 23:31:17 -0600 Subject: [PATCH 07/13] explicit key presence check in by_format, comment adjusted Signed-off-by: Vijay Kumar Soni --- .../protocols/present_proof/v2_0/models/pres_exchange.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py b/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py index 46139d546e..8164e33d84 100644 --- a/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py +++ b/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py @@ -206,10 +206,11 @@ async def emit_event(self, session: ProfileSession, payload: Optional[Any] = Non if not session.profile.settings.get("debug.webhooks"): payload = V20PresExRecordWebhook(**payload) payload = payload.__dict__ - # BUG #3802: remove legacy fields when by_format is present - elif payload.get("by_format"): + elif by_format := payload.get("by_format"): + # Issue #3802: remove legacy fields only when represented in by_format for key in ("pres_proposal", "pres_request", "pres"): - payload.pop(key, None) + if key in by_format: + payload.pop(key, None) await session.emit_event(topic, payload) From da848506312dd249b925a49eccd0a8012a1ed8ac Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Thu, 19 Feb 2026 23:39:34 -0600 Subject: [PATCH 08/13] added assertion to check 3 keys pres,pres_proposal, pres_request Signed-off-by: Vijay Kumar Soni --- .../v2_0/models/tests/test_record.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py b/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py index 701fba8e0c..5b12143c01 100644 --- a/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py +++ b/acapy_agent/protocols/present_proof/v2_0/models/tests/test_record.py @@ -170,6 +170,19 @@ async def test_emit_event_strips_legacy_pres_fields(self): AttachDecorator.data_base64(mapping=INDY_PROOF_REQ, ident="indy") ], ) + pres_proposal = V20PresProposal( + formats=[ + V20PresFormat( + attach_id="indy", + format_=ATTACHMENT_FORMAT[PRES_20_PROPOSAL][ + V20PresFormat.Format.INDY.api + ], + ) + ], + proposals_attach=[ + AttachDecorator.data_base64(mapping=INDY_PROOF_REQ, ident="indy") + ], + ) pres = V20Pres( formats=[ V20PresFormat( @@ -188,6 +201,7 @@ async def test_emit_event_strips_legacy_pres_fields(self): initiator="init", role="role", state=V20PresExRecord.STATE_PRESENTATION_RECEIVED, + pres_proposal=pres_proposal, pres_request=pres_request, pres=pres, ) @@ -198,6 +212,5 @@ async def test_emit_event_strips_legacy_pres_fields(self): payload = session.emit_event.call_args.args[1] assert "by_format" in payload - assert "pres_request" not in payload - assert "pres" not in payload - assert "pres_proposal" not in payload + for key in ("pres", "pres_proposal", "pres_request"): + assert key not in payload and key in payload["by_format"] From 4255af0d7556702ebf71c6bfdaea235f44a2ddea Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Thu, 19 Feb 2026 23:42:15 -0600 Subject: [PATCH 09/13] corrected proof_request selection logic Signed-off-by: Vijay Kumar Soni --- scenarios/examples/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index 136b84df54..7791fc3278 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -628,11 +628,10 @@ async def anoncreds_present_proof_v2( request_payload = _presentation_request_payload(holder_pres_ex) assert request_payload + proof_request = request_payload if "anoncreds" in request_payload or "indy" in request_payload: proof_request = request_payload.get("anoncreds") or request_payload.get("indy") - else: - proof_request = request_payload - assert proof_request + assert proof_request pres_spec = auto_select_credentials_for_presentation_request( proof_request, relevant_creds ) From 8e7894d77f3c1da3b11050eb3d4c816c3858dde9 Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Thu, 19 Feb 2026 23:44:53 -0600 Subject: [PATCH 10/13] corrected proof_request fallback logic Signed-off-by: Vijay Kumar Soni --- scenarios/examples/util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index 7791fc3278..912ec63a46 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -271,11 +271,10 @@ async def indy_present_proof_v2( request_payload = _presentation_request_payload(holder_pres_ex) assert request_payload + proof_request = request_payload if "anoncreds" in request_payload or "indy" in request_payload: proof_request = request_payload.get("indy") or request_payload.get("anoncreds") - else: - proof_request = request_payload - assert proof_request + assert proof_request pres_spec = auto_select_credentials_for_presentation_request( proof_request, relevant_creds ) From 3ff63579de2b7ed1319df28a729e5f8fa0f875d3 Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Thu, 19 Feb 2026 23:46:59 -0600 Subject: [PATCH 11/13] indy_present_proof_v2 logic Signed-off-by: Vijay Kumar Soni --- scenarios/examples/util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index 912ec63a46..aeb6866aac 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -222,7 +222,12 @@ async def indy_present_proof_v2( non_revoked: Optional[Mapping[str, int]] = None, cred_rev_id: Optional[str] = None, ): - """Present a credential using present proof v2 (indy).""" + """Present a credential using present proof v2 (indy). + + This follows the acapy_controller.protocols flow, but resolves the holder-side + request payload via _presentation_request_payload(...) to support both legacy + pres_request and by_format webhook payload shapes. + """ attrs = { "name": name or "proof", "version": version or "0.1.0", From ac95e4bed955dad371b6912d5e5675f52ace6adc Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Thu, 19 Feb 2026 23:52:25 -0600 Subject: [PATCH 12/13] fixed util.py, examples Signed-off-by: Vijay Kumar Soni --- .../did_indy_issuance_and_revocation/example.py | 15 +-------------- .../kanon_issuance_and_presentation/example.py | 14 +------------- .../presenting_revoked_credential/example.py | 15 +-------------- scenarios/examples/util.py | 2 +- 4 files changed, 4 insertions(+), 42 deletions(-) diff --git a/scenarios/examples/did_indy_issuance_and_revocation/example.py b/scenarios/examples/did_indy_issuance_and_revocation/example.py index 2de5d4a201..358f537d51 100644 --- a/scenarios/examples/did_indy_issuance_and_revocation/example.py +++ b/scenarios/examples/did_indy_issuance_and_revocation/example.py @@ -21,25 +21,12 @@ params, ) from aiohttp import ClientSession -from examples.util import indy_present_proof_v2 +from examples.util import _presentation_request_payload, indy_present_proof_v2 ALICE = getenv("ALICE", "http://alice:3001") BOB = getenv("BOB", "http://bob:3001") -def _presentation_request_payload(presentation: V20PresExRecord): - if presentation.by_format and presentation.by_format.pres_request: - return presentation.by_format.pres_request - request = presentation.pres_request - if not request: - return None - if isinstance(request, dict): - return request - if hasattr(request, "model_dump"): - return request.model_dump(by_alias=True) - return request.dict(by_alias=True) - - def summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" request = _presentation_request_payload(presentation) diff --git a/scenarios/examples/kanon_issuance_and_presentation/example.py b/scenarios/examples/kanon_issuance_and_presentation/example.py index 7cff659567..80cd14cec9 100644 --- a/scenarios/examples/kanon_issuance_and_presentation/example.py +++ b/scenarios/examples/kanon_issuance_and_presentation/example.py @@ -19,6 +19,7 @@ ) from aiohttp import ClientSession from examples.util import ( + _presentation_request_payload, CredDefResultAnonCreds, SchemaResultAnonCreds, anoncreds_issue_credential_v2, @@ -29,19 +30,6 @@ BOB = getenv("BOB", "http://bob:3001") -def _presentation_request_payload(presentation: V20PresExRecord): - if presentation.by_format and presentation.by_format.pres_request: - return presentation.by_format.pres_request - request = presentation.pres_request - if not request: - return None - if isinstance(request, dict): - return request - if hasattr(request, "model_dump"): - return request.model_dump(by_alias=True) - return request.dict(by_alias=True) - - def summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" request = _presentation_request_payload(presentation) diff --git a/scenarios/examples/presenting_revoked_credential/example.py b/scenarios/examples/presenting_revoked_credential/example.py index 97d8ef54f0..c1c2e70320 100644 --- a/scenarios/examples/presenting_revoked_credential/example.py +++ b/scenarios/examples/presenting_revoked_credential/example.py @@ -21,25 +21,12 @@ params, ) from aiohttp import ClientSession -from examples.util import indy_present_proof_v2 +from examples.util import _presentation_request_payload, indy_present_proof_v2 ALICE = getenv("ALICE", "http://alice:3001") BOB = getenv("BOB", "http://bob:3001") -def _presentation_request_payload(presentation: V20PresExRecord): - if presentation.by_format and presentation.by_format.pres_request: - return presentation.by_format.pres_request - request = presentation.pres_request - if not request: - return None - if isinstance(request, dict): - return request - if hasattr(request, "model_dump"): - return request.model_dump(by_alias=True) - return request.dict(by_alias=True) - - def summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" request = _presentation_request_payload(presentation) diff --git a/scenarios/examples/util.py b/scenarios/examples/util.py index aeb6866aac..83ebc195ae 100644 --- a/scenarios/examples/util.py +++ b/scenarios/examples/util.py @@ -59,7 +59,6 @@ def get_wallet_name(agent_command: List) -> str: raise Exception("Error unable to upgrade wallet type to askar-anoncreds") -# anoncreds utilities: def _presentation_request_payload( presentation: V20PresExRecord, ) -> Optional[Dict[str, Any]]: @@ -75,6 +74,7 @@ def _presentation_request_payload( return request.dict(by_alias=True) +# anoncreds utilities: def anoncreds_presentation_summary(presentation: V20PresExRecord) -> str: """Summarize a presentation exchange record.""" request = _presentation_request_payload(presentation) From 8f66f84205fec4515158e4ac085f614f931a0270 Mon Sep 17 00:00:00 2001 From: Vijay Kumar Soni Date: Fri, 20 Feb 2026 00:18:54 -0600 Subject: [PATCH 13/13] fixed formatting linting issue Signed-off-by: Vijay Kumar Soni --- .../protocols/present_proof/v2_0/models/pres_exchange.py | 2 +- scenarios/examples/json_ld/example.py | 5 +---- .../examples/kanon_issuance_and_presentation/example.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py b/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py index 8164e33d84..b2b1fc873f 100644 --- a/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py +++ b/acapy_agent/protocols/present_proof/v2_0/models/pres_exchange.py @@ -207,7 +207,7 @@ async def emit_event(self, session: ProfileSession, payload: Optional[Any] = Non payload = V20PresExRecordWebhook(**payload) payload = payload.__dict__ elif by_format := payload.get("by_format"): - # Issue #3802: remove legacy fields only when represented in by_format + # Issue #3802: remove legacy fields only when represented in by_format for key in ("pres_proposal", "pres_request", "pres"): if key in by_format: payload.pop(key, None) diff --git a/scenarios/examples/json_ld/example.py b/scenarios/examples/json_ld/example.py index b45b326baf..eda55fc7da 100644 --- a/scenarios/examples/json_ld/example.py +++ b/scenarios/examples/json_ld/example.py @@ -404,10 +404,7 @@ async def main(): options={"proofType": "BbsBlsSignature2020"}, ) except ControllerError as err: - print( - "Skipping BBS+ flow due to runtime capability/error: " - f"{err}" - ) + print(f"Skipping BBS+ flow due to runtime capability/error: {err}") return pause_for_input() diff --git a/scenarios/examples/kanon_issuance_and_presentation/example.py b/scenarios/examples/kanon_issuance_and_presentation/example.py index 80cd14cec9..92b47d0d18 100644 --- a/scenarios/examples/kanon_issuance_and_presentation/example.py +++ b/scenarios/examples/kanon_issuance_and_presentation/example.py @@ -19,9 +19,9 @@ ) from aiohttp import ClientSession from examples.util import ( - _presentation_request_payload, CredDefResultAnonCreds, SchemaResultAnonCreds, + _presentation_request_payload, anoncreds_issue_credential_v2, anoncreds_present_proof_v2, )