From 954af7dab976225fb1f9a0ef19866637abd54f03 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Tue, 12 May 2026 14:19:01 +0200 Subject: [PATCH 1/3] Workflows: Add JSONPatch events partial decryption --- .../workflows/encoding/payload_encoder.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/mistralai/extra/workflows/encoding/payload_encoder.py b/src/mistralai/extra/workflows/encoding/payload_encoder.py index 611f33fa..226e57c0 100644 --- a/src/mistralai/extra/workflows/encoding/payload_encoder.py +++ b/src/mistralai/extra/workflows/encoding/payload_encoder.py @@ -339,6 +339,21 @@ async def decode_event_payload( return payload_data encoding_options = [EncodedPayloadOptions(opt) for opt in encoding_options_strs] + + # Handle selective encryption for json_patch payloads + if ( + EncodedPayloadOptions.PARTIALLY_ENCRYPTED in encoding_options + and payload_data.get("type") == "json_patch" + and isinstance(payload_data.get("value"), list) + ): + decrypted_patches = self._decrypt_json_patch_selective(payload_data["value"]) + return { + "type": payload_data["type"], + "value": decrypted_patches, + "encoding_options": [], + } + + # Standard full encryption (base64 string value) encrypted_bytes = base64.b64decode(payload_data["value"]) decrypted_bytes = await self.decode_payload_content(encrypted_bytes, encoding_options) decrypted_value = json.loads(decrypted_bytes) @@ -349,6 +364,26 @@ async def decode_event_payload( "encoding_options": [], } + _ENCRYPTED_PATCH_KEY = "__encrypted__" + + def _decrypt_json_patch_selective(self, patches: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Decrypt patches with EncryptedPatchValue wrapper (dict with __encrypted__ key).""" + decrypted = [] + for patch in patches: + patch_value = patch.get("value") + + if isinstance(patch_value, dict) and self._ENCRYPTED_PATCH_KEY in patch_value: + encrypted_b64 = patch_value[self._ENCRYPTED_PATCH_KEY] + encrypted_data = base64.b64decode(encrypted_b64) + decrypted_bytes = self._decrypt(encrypted_data) + decrypted.append({ + **patch, + "value": json.loads(decrypted_bytes), + }) + else: + decrypted.append(patch) + return decrypted + async def encode_network_input( self, data: Optional[Dict[str, Any]], context: WorkflowContext ) -> NetworkEncodedInput: From c2f21854e8f52e183bb648e17dd3431954c93664 Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Mon, 18 May 2026 14:09:26 +0200 Subject: [PATCH 2/3] Update to new format --- .../extra/workflows/encoding/payload_encoder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mistralai/extra/workflows/encoding/payload_encoder.py b/src/mistralai/extra/workflows/encoding/payload_encoder.py index 226e57c0..e521163f 100644 --- a/src/mistralai/extra/workflows/encoding/payload_encoder.py +++ b/src/mistralai/extra/workflows/encoding/payload_encoder.py @@ -364,16 +364,17 @@ async def decode_event_payload( "encoding_options": [], } - _ENCRYPTED_PATCH_KEY = "__encrypted__" + _ENCRYPTED_PATCH_TYPE = "__encrypted__" def _decrypt_json_patch_selective(self, patches: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Decrypt patches with EncryptedPatchValue wrapper (dict with __encrypted__ key).""" + """Decrypt patches with EncryptedPatchValue wrapper: {type: "__encrypted__", value: "base64..."}.""" decrypted = [] for patch in patches: patch_value = patch.get("value") - if isinstance(patch_value, dict) and self._ENCRYPTED_PATCH_KEY in patch_value: - encrypted_b64 = patch_value[self._ENCRYPTED_PATCH_KEY] + # EncryptedPatchValue format: {"type": "__encrypted__", "value": "base64-encrypted-data"} + if isinstance(patch_value, dict) and patch_value.get("type") == self._ENCRYPTED_PATCH_TYPE: + encrypted_b64 = patch_value.get("value", "") encrypted_data = base64.b64decode(encrypted_b64) decrypted_bytes = self._decrypt(encrypted_data) decrypted.append({ From 66ae449b2042cc5236141a009f21e8fcedf36c4a Mon Sep 17 00:00:00 2001 From: Nicolas Faurie Date: Thu, 21 May 2026 14:01:37 +0200 Subject: [PATCH 3/3] Improvement --- .../workflows/encoding/payload_encoder.py | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/mistralai/extra/workflows/encoding/payload_encoder.py b/src/mistralai/extra/workflows/encoding/payload_encoder.py index e521163f..1a7fe7ae 100644 --- a/src/mistralai/extra/workflows/encoding/payload_encoder.py +++ b/src/mistralai/extra/workflows/encoding/payload_encoder.py @@ -9,7 +9,7 @@ import urllib.parse from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ValidationError if TYPE_CHECKING: from cryptography.exceptions import InvalidTag @@ -38,6 +38,7 @@ NetworkEncodedResult, WorkflowContext, ) +from mistralai.client.models.jsonpatchpayloadresponse import JSONPatchPayloadResponse from mistralai.extra.exceptions import ( WorkflowPayloadEncryptionException, WorkflowPayloadOffloadingException, @@ -151,7 +152,10 @@ def _decrypt(self, data: bytes) -> bytes: async def _handle_offloading( self, data: bytes, context: Optional[WorkflowContext] ) -> tuple[bytes, bool]: - if self.offloading_config is None or self.offloading_config.storage_config is None: + if ( + self.offloading_config is None + or self.offloading_config.storage_config is None + ): raise WorkflowPayloadOffloadingException( "You must configure payload offloading storage" ) @@ -281,7 +285,10 @@ async def encode_event_payload_content( if self.encryption_config is None: return data, [] - if force_full_encryption or self.encryption_config.mode == PayloadEncryptionMode.FULL: + if ( + force_full_encryption + or self.encryption_config.mode == PayloadEncryptionMode.FULL + ): encrypted_data = self._encrypt(data) return encrypted_data, [EncodedPayloadOptions.ENCRYPTED] @@ -341,21 +348,26 @@ async def decode_event_payload( encoding_options = [EncodedPayloadOptions(opt) for opt in encoding_options_strs] # Handle selective encryption for json_patch payloads - if ( - EncodedPayloadOptions.PARTIALLY_ENCRYPTED in encoding_options - and payload_data.get("type") == "json_patch" - and isinstance(payload_data.get("value"), list) - ): - decrypted_patches = self._decrypt_json_patch_selective(payload_data["value"]) - return { - "type": payload_data["type"], - "value": decrypted_patches, - "encoding_options": [], - } + if EncodedPayloadOptions.PARTIALLY_ENCRYPTED in encoding_options: + try: + payload = JSONPatchPayloadResponse.model_validate(payload_data) + if isinstance(payload.value, list): + decrypted_patches = self._decrypt_json_patch_selective( + [p.model_dump() for p in payload.value] + ) + return { + "type": payload.type, + "value": decrypted_patches, + "encoding_options": [], + } + except ValidationError: + pass # Not a json_patch payload, fall through to full decryption # Standard full encryption (base64 string value) encrypted_bytes = base64.b64decode(payload_data["value"]) - decrypted_bytes = await self.decode_payload_content(encrypted_bytes, encoding_options) + decrypted_bytes = await self.decode_payload_content( + encrypted_bytes, encoding_options + ) decrypted_value = json.loads(decrypted_bytes) return { @@ -366,21 +378,28 @@ async def decode_event_payload( _ENCRYPTED_PATCH_TYPE = "__encrypted__" - def _decrypt_json_patch_selective(self, patches: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _decrypt_json_patch_selective( + self, patches: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: """Decrypt patches with EncryptedPatchValue wrapper: {type: "__encrypted__", value: "base64..."}.""" decrypted = [] for patch in patches: patch_value = patch.get("value") # EncryptedPatchValue format: {"type": "__encrypted__", "value": "base64-encrypted-data"} - if isinstance(patch_value, dict) and patch_value.get("type") == self._ENCRYPTED_PATCH_TYPE: + if ( + isinstance(patch_value, dict) + and patch_value.get("type") == self._ENCRYPTED_PATCH_TYPE + ): encrypted_b64 = patch_value.get("value", "") encrypted_data = base64.b64decode(encrypted_b64) decrypted_bytes = self._decrypt(encrypted_data) - decrypted.append({ - **patch, - "value": json.loads(decrypted_bytes), - }) + decrypted.append( + { + **patch, + "value": json.loads(decrypted_bytes), + } + ) else: decrypted.append(patch) return decrypted