From 95cea8f2bd70e7c22045fc19abbac2358b9b7844 Mon Sep 17 00:00:00 2001 From: iFergal Date: Thu, 26 Mar 2026 11:07:36 +0000 Subject: [PATCH 1/9] chore: fix generation of ACDC ordering and extra fields --- src/keria/app/credentialing.py | 2 + src/keria/utils/openapi.py | 93 ++++++++++------------------------ 2 files changed, 30 insertions(+), 65 deletions(-) diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index 4233662..d43121a 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -93,6 +93,8 @@ class ACDCAttributes: acdcCustomTypes = { "a": ACDCAttributes, "A": Union[str, List[Any]], + "e": Dict[str, Any], + "r": Dict[str, Any] } acdcFieldDomV1 = SerderKERI.Fields[Protocols.acdc][Vrsn_1_0][None] ACDC_V_1, ACDCSchema_V_1 = dataclassFromFielddom( diff --git a/src/keria/utils/openapi.py b/src/keria/utils/openapi.py index d399d1a..894c6de 100644 --- a/src/keria/utils/openapi.py +++ b/src/keria/utils/openapi.py @@ -235,6 +235,13 @@ def createCustomNestedField( return createOptionalField( key, customType, mm_fields.List, (nestedField,), {}, isOptional ) + elif customType.__origin__ is dict: + return createOptionalField( + key, customType, mm_fields.Dict, + (), + {"keys": mm_fields.String(), "values": mm_fields.Raw()}, + isOptional + ) # For other generic types, fall back to Raw field return createOptionalField(key, customType, mm_fields.Raw, (), {}, isOptional) @@ -291,12 +298,11 @@ def createRegularField( def processField( key: str, value: Any, - fieldDom: serdering.FieldDom, + isOptional: bool, customTypes: Dict[str, type], name: str = "", ) -> tuple[tuple, Optional[mm_fields.Field]]: """Process a single field from the FieldDom.""" - isOptional = key in fieldDom.opts # Check if there's a custom type specified if key in customTypes: @@ -328,8 +334,7 @@ def dataclassFromFielddom( if customTypes is None: customTypes = {} - requiredFields = [] - optionalFields = [] + allFields = [] customFields = {} # Store alt constraints for use by schema generation @@ -337,24 +342,18 @@ def dataclassFromFielddom( # Process all fields from alls (all possible fields) for key, value in fieldDom.alls.items(): + isOptional = key in fieldDom.opts fieldDef, marshmallowField = processField( - key, value, fieldDom, customTypes, name + key, value, isOptional, customTypes, name ) if marshmallowField: customFields[key] = marshmallowField - # Check if field is optional (in opts) - isOptional = key in fieldDom.opts - if isOptional: - optionalFields.append(fieldDef) - else: - requiredFields.append(fieldDef) + allFields.append(fieldDef) - allFields = requiredFields + optionalFields - generatedCls = make_dataclass(name, allFields) + generatedCls = make_dataclass(name, allFields, kw_only=True) schema = class_schema(generatedCls)() - # Override the automatically generated fields with our custom ones # marshmallow_dataclass automatically creates fields with allow_none=True for Optional[T] # We need to override them to have allow_none=False for openapi-typescript compatibility @@ -385,62 +384,26 @@ def applyAltConstraintsToOpenApiSchema( properties = openApiSchemaDict.get("properties", {}) required = openApiSchemaDict.get("required", []) + baseRequired = [f for f in required if f not in altConstraints] - # Find alternate field pairs that exist in properties - altGroups = {} - processedAlts = set() - - for field1, field2 in altConstraints.items(): - if field1 in processedAlts or field2 in processedAlts: - continue - if field1 in properties and field2 in properties: - groupKey = f"{field1}_{field2}" - altGroups[groupKey] = [field1, field2] - processedAlts.add(field1) - processedAlts.add(field2) - - if not altGroups: - return - - # Create oneOf schemas for alternate field combinations oneOfSchemas = [] - - for _, altFields in altGroups.items(): - field1, field2 = altFields - - # Base properties (all except alternates) - baseProps = {k: v for k, v in properties.items() if k not in altFields} - baseRequired = [f for f in required if f not in altFields] - - # Schema with field1 only - schemaWithField1 = { - "type": "object", - "properties": {**baseProps, field1: properties[field1]}, - "additionalProperties": openApiSchemaDict.get( - "additionalProperties", False - ), - } - if field1 in required or baseRequired: - schemaWithField1["required"] = baseRequired + ( - [field1] if field1 in required else [] - ) - oneOfSchemas.append(schemaWithField1) - - # Schema with field2 only - schemaWithField2 = { + # Note: For now, this only works with 1 pair of alts - if more ever come, we need to start computing permutations. + for keepField in altConstraints: + if keepField not in properties: + continue + variant = { "type": "object", - "properties": {**baseProps, field2: properties[field2]}, - "additionalProperties": openApiSchemaDict.get( - "additionalProperties", False - ), + "properties": { + k: v for k, v in properties.items() + if k == keepField or k not in altConstraints + }, + "additionalProperties": openApiSchemaDict.get("additionalProperties", False), } - if field2 in required or baseRequired: - schemaWithField2["required"] = baseRequired + ( - [field2] if field2 in required else [] - ) - oneOfSchemas.append(schemaWithField2) + variantRequired = baseRequired + ([keepField] if keepField in required else []) + if variantRequired: + variant["required"] = variantRequired + oneOfSchemas.append(variant) - # Replace the schema with oneOf constraint if oneOfSchemas: openApiSchemaDict.clear() openApiSchemaDict["oneOf"] = oneOfSchemas From 25fc5b5aba603bb5dff1422ee37d62d8f8e7cb51 Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Thu, 26 Mar 2026 18:26:23 +0700 Subject: [PATCH 2/9] fix CredentialState schema --- src/keria/app/specing.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/keria/app/specing.py b/src/keria/app/specing.py index 6d078e4..66671da 100644 --- a/src/keria/app/specing.py +++ b/src/keria/app/specing.py @@ -178,7 +178,26 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): "oneOf": [ {"$ref": "#/components/schemas/CredentialStateIssOrRev"}, {"$ref": "#/components/schemas/CredentialStateBisOrBrv"}, - ] + ], + "properties": { + "et": { + "type": "string", + "enum": ["iss", "rev", "bis", "brv"], + }, + "ra": { + "type": "object", + "description": "Empty for iss/rev, RaFields for bis/brv", + }, + }, + "discriminator": { + "propertyName": "et", + "mapping": { + "iss": "#/components/schemas/CredentialStateIssOrRev", + "rev": "#/components/schemas/CredentialStateIssOrRev", + "bis": "#/components/schemas/CredentialStateBisOrBrv", + "brv": "#/components/schemas/CredentialStateBisOrBrv", + }, + }, } credentialSchema["properties"]["status"] = { From 104c81f8831f00f5d99a80f95a5c9a42c25d6d43 Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 27 Mar 2026 12:03:30 +0000 Subject: [PATCH 3/9] fix: ancatc type --- src/keria/app/credentialing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index d43121a..f1e8e92 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -203,7 +203,7 @@ class ClonedCredential: status: Union[CredentialStateIssOrRev, CredentialStateBisOrBrv] anchor: Anchor anc: AnchoringEvent # type: ignore - ancatc: str + ancatc: List[str] @dataclass @@ -832,6 +832,7 @@ def on_post(req, rep): rep.status = falcon.HTTP_200 rep.content_type = "application/json" + print(f"rep.data is {json.dumps(creds).encode("utf-8")}") rep.data = json.dumps(creds).encode("utf-8") From 05e0f38156b2b0bab6ce2681ed2aa149d19eae4d Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 27 Mar 2026 17:12:29 +0000 Subject: [PATCH 4/9] refactor: remove print --- src/keria/app/credentialing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index f1e8e92..f69c570 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -832,7 +832,6 @@ def on_post(req, rep): rep.status = falcon.HTTP_200 rep.content_type = "application/json" - print(f"rep.data is {json.dumps(creds).encode("utf-8")}") rep.data = json.dumps(creds).encode("utf-8") From 0c62d47b0c74c0170b34a33304a70a22fa9b2977 Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 27 Mar 2026 17:27:56 +0000 Subject: [PATCH 5/9] refactor: formatting --- src/keria/app/credentialing.py | 2 +- src/keria/utils/openapi.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index f69c570..af4db83 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -94,7 +94,7 @@ class ACDCAttributes: "a": ACDCAttributes, "A": Union[str, List[Any]], "e": Dict[str, Any], - "r": Dict[str, Any] + "r": Dict[str, Any], } acdcFieldDomV1 = SerderKERI.Fields[Protocols.acdc][Vrsn_1_0][None] ACDC_V_1, ACDCSchema_V_1 = dataclassFromFielddom( diff --git a/src/keria/utils/openapi.py b/src/keria/utils/openapi.py index 894c6de..19c0191 100644 --- a/src/keria/utils/openapi.py +++ b/src/keria/utils/openapi.py @@ -237,10 +237,12 @@ def createCustomNestedField( ) elif customType.__origin__ is dict: return createOptionalField( - key, customType, mm_fields.Dict, + key, + customType, + mm_fields.Dict, (), {"keys": mm_fields.String(), "values": mm_fields.Raw()}, - isOptional + isOptional, ) # For other generic types, fall back to Raw field @@ -394,10 +396,13 @@ def applyAltConstraintsToOpenApiSchema( variant = { "type": "object", "properties": { - k: v for k, v in properties.items() + k: v + for k, v in properties.items() if k == keepField or k not in altConstraints }, - "additionalProperties": openApiSchemaDict.get("additionalProperties", False), + "additionalProperties": openApiSchemaDict.get( + "additionalProperties", False + ), } variantRequired = baseRequired + ([keepField] if keepField in required else []) if variantRequired: From f9e023e11c9827a1313faa158c48cb1c41b8bd15 Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 27 Mar 2026 21:52:42 +0000 Subject: [PATCH 6/9] fix: correct registry state dataclass for openapi spec --- src/keria/app/credentialing.py | 18 ++++++++++++++++-- src/keria/app/specing.py | 4 ---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index af4db83..135ab6e 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -133,7 +133,7 @@ class Schema: @dataclass class CredentialStateBase: - vn: Tuple[int, int] + vn: List[int] i: str s: str d: str @@ -206,12 +206,26 @@ class ClonedCredential: ancatc: List[str] +@dataclass +class RegistryState: + vn: List[int] + i: str + s: str + d: str + ii: str + dt: str + et: Literal["vcp", "vrt"] + bt: str + b: List[str] + c: List[str] + + @dataclass class Registry: name: str regk: str pre: str - state: Union[CredentialStateIssOrRev, CredentialStateBisOrBrv] + state: RegistryState class RegistryCollectionEnd: diff --git a/src/keria/app/specing.py b/src/keria/app/specing.py index 66671da..b082264 100644 --- a/src/keria/app/specing.py +++ b/src/keria/app/specing.py @@ -214,10 +214,6 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): "Registry", schema=marshmallow_dataclass.class_schema(credentialing.Registry)(), ) - registrySchema = self.spec.components.schemas["Registry"] - registrySchema["properties"]["state"] = { - "$ref": "#/components/schemas/CredentialState" - } self.spec.components.schema( "AgentResourceResult", From 49abafb010b262888ca469725dd17f929928088d Mon Sep 17 00:00:00 2001 From: iFergal Date: Fri, 27 Mar 2026 21:54:28 +0000 Subject: [PATCH 7/9] refactor: remove unused import --- src/keria/app/credentialing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keria/app/credentialing.py b/src/keria/app/credentialing.py index 135ab6e..3a68ffe 100644 --- a/src/keria/app/credentialing.py +++ b/src/keria/app/credentialing.py @@ -22,7 +22,7 @@ from keri.core.serdering import Protocols, Vrsn_1_0, Vrsn_2_0, SerderKERI from ..core import httping, longrunning from marshmallow import fields, Schema as MarshmallowSchema -from typing import List, Dict, Any, Optional, Tuple, Literal, Union +from typing import List, Dict, Any, Optional, Literal, Union from .aiding import ( Seal, ICP_V_1, From f96ad27d07b48db5dce21d60114f2847a285e76b Mon Sep 17 00:00:00 2001 From: iFergal Date: Sat, 28 Mar 2026 13:28:45 +0000 Subject: [PATCH 8/9] fix: better specify keyevents and fix group member ends --- src/keria/app/aiding.py | 18 +++++++++--------- src/keria/app/specing.py | 5 +++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/keria/app/aiding.py b/src/keria/app/aiding.py index 5eaede1..e64f245 100644 --- a/src/keria/app/aiding.py +++ b/src/keria/app/aiding.py @@ -2109,15 +2109,15 @@ class WellKnown: @dataclass class MemberEnds: - agent: Optional[Dict[str, str]] = None - controller: Optional[Dict[str, str]] = None - witness: Optional[Dict[str, str]] = None - registrar: Optional[Dict[str, str]] = None - watcher: Optional[Dict[str, str]] = None - judge: Optional[Dict[str, str]] = None - juror: Optional[Dict[str, str]] = None - peer: Optional[Dict[str, str]] = None - mailbox: Optional[Dict[str, str]] = None + agent: Optional[Dict[str, Dict[str, str]]] = None + controller: Optional[Dict[str, Dict[str, str]]] = None + witness: Optional[Dict[str, Dict[str, str]]] = None + registrar: Optional[Dict[str, Dict[str, str]]] = None + watcher: Optional[Dict[str, Dict[str, str]]] = None + judge: Optional[Dict[str, Dict[str, str]]] = None + juror: Optional[Dict[str, Dict[str, str]]] = None + peer: Optional[Dict[str, Dict[str, str]]] = None + mailbox: Optional[Dict[str, Dict[str, str]]] = None @dataclass diff --git a/src/keria/app/specing.py b/src/keria/app/specing.py index f4a2b6a..6e1b860 100644 --- a/src/keria/app/specing.py +++ b/src/keria/app/specing.py @@ -175,7 +175,8 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): {"$ref": "#/components/schemas/DRT_V_2"}, ] } - credentialSchema["properties"]["anc"] = ancEvent + self.spec.components.schemas["KeyEvent"] = ancEvent + credentialSchema["properties"]["anc"] = {"$ref": "#/components/schemas/KeyEvent"} # CredentialState self.spec.components.schemas["CredentialState"] = { @@ -329,7 +330,7 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): schema=marshmallow_dataclass.class_schema(agenting.KeyEventRecord)(), ) keyEventRecordSchema = self.spec.components.schemas["KeyEventRecord"] - keyEventRecordSchema["properties"]["ked"] = ancEvent + keyEventRecordSchema["properties"]["ked"] = {"$ref": "#/components/schemas/KeyEvent"} # Register the AgentConfig schema self.spec.components.schema( From cb4015210ab77bbe815400957c9ad5366d31d58e Mon Sep 17 00:00:00 2001 From: iFergal Date: Sat, 28 Mar 2026 13:29:50 +0000 Subject: [PATCH 9/9] format --- src/keria/app/specing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/keria/app/specing.py b/src/keria/app/specing.py index 6e1b860..fd3eeeb 100644 --- a/src/keria/app/specing.py +++ b/src/keria/app/specing.py @@ -176,7 +176,9 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): ] } self.spec.components.schemas["KeyEvent"] = ancEvent - credentialSchema["properties"]["anc"] = {"$ref": "#/components/schemas/KeyEvent"} + credentialSchema["properties"]["anc"] = { + "$ref": "#/components/schemas/KeyEvent" + } # CredentialState self.spec.components.schemas["CredentialState"] = { @@ -330,7 +332,9 @@ def __init__(self, app, title, version="1.0.1", openapi_version="3.1.0"): schema=marshmallow_dataclass.class_schema(agenting.KeyEventRecord)(), ) keyEventRecordSchema = self.spec.components.schemas["KeyEventRecord"] - keyEventRecordSchema["properties"]["ked"] = {"$ref": "#/components/schemas/KeyEvent"} + keyEventRecordSchema["properties"]["ked"] = { + "$ref": "#/components/schemas/KeyEvent" + } # Register the AgentConfig schema self.spec.components.schema(