workflow-exec-engine 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. workflow_engine/__init__.py +95 -0
  2. workflow_engine/client/__init__.py +47 -0
  3. workflow_engine/client/a2a_transport.py +560 -0
  4. workflow_engine/client/agentcard_normalizer.py +106 -0
  5. workflow_engine/client/auth_manager.py +127 -0
  6. workflow_engine/client/auth_provider.py +47 -0
  7. workflow_engine/client/credential_crypto.py +102 -0
  8. workflow_engine/client/credential_service.py +229 -0
  9. workflow_engine/client/engine_client.py +374 -0
  10. workflow_engine/client/env_file_loader.py +68 -0
  11. workflow_engine/client/extension_handlers.py +197 -0
  12. workflow_engine/client/extension_interceptor.py +76 -0
  13. workflow_engine/client/extension_sender.py +203 -0
  14. workflow_engine/client/extensions.py +43 -0
  15. workflow_engine/client/protocol_logger.py +78 -0
  16. workflow_engine/client/sse_normalization.py +87 -0
  17. workflow_engine/client/ssl_context.py +84 -0
  18. workflow_engine/client/stub_engine_client.py +68 -0
  19. workflow_engine/control/__init__.py +26 -0
  20. workflow_engine/control/control_points.py +223 -0
  21. workflow_engine/core/__init__.py +34 -0
  22. workflow_engine/core/context_builder.py +101 -0
  23. workflow_engine/core/executor.py +278 -0
  24. workflow_engine/core/models.py +184 -0
  25. workflow_engine/registry/__init__.py +21 -0
  26. workflow_engine/registry/registry_client.py +177 -0
  27. workflow_engine/runner.py +247 -0
  28. workflow_exec_engine-0.0.2.dist-info/METADATA +309 -0
  29. workflow_exec_engine-0.0.2.dist-info/RECORD +32 -0
  30. workflow_exec_engine-0.0.2.dist-info/WHEEL +5 -0
  31. workflow_exec_engine-0.0.2.dist-info/licenses/LICENSE +17 -0
  32. workflow_exec_engine-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,127 @@
1
+ # Copyright (c) 2026 Huawei Technologies Co., Ltd.
2
+ # All Rights Reserved.
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ """Auth manager - builds interceptors from AgentCard, self-contained."""
19
+
20
+ from typing import Dict, Any, List, Optional
21
+ from loguru import logger
22
+
23
+ try:
24
+ from a2a.client.auth import AuthInterceptor
25
+ from a2a.client.interceptors import ClientCallInterceptor, BeforeArgs, AfterArgs
26
+ _A2A_AUTH_AVAILABLE = True
27
+ except ImportError:
28
+ _A2A_AUTH_AVAILABLE = False
29
+ AuthInterceptor = None
30
+ ClientCallInterceptor = None
31
+ BeforeArgs = None
32
+ AfterArgs = None
33
+
34
+
35
+ from workflow_engine.client.credential_service import AgentAuthManager, CustomAuthInterceptor
36
+ from workflow_engine.client.extension_interceptor import ExtensionInterceptor
37
+ from workflow_engine.client.auth_provider import AuthProvider
38
+
39
+
40
+ class AuthManager:
41
+ """Builds auth/extension interceptors from AgentCard securitySchemes."""
42
+
43
+ def __init__(self, agent_cards: List[Any], credentials_config: Optional[str | Dict] = None):
44
+ self._interceptors: Dict[str, List[Any]] = {}
45
+ self._auth_manager: Optional[AgentAuthManager] = None
46
+
47
+ if not _A2A_AUTH_AVAILABLE:
48
+ logger.info("[AuthManager] a2a-sdk auth not available, authentication disabled")
49
+ return
50
+ logger.info(f"[AuthManager] Initializing with {len(agent_cards)} agent card(s), config={credentials_config is not None}")
51
+
52
+ if credentials_config:
53
+ if isinstance(credentials_config, str):
54
+ self._auth_manager = AgentAuthManager(config_path=credentials_config)
55
+ elif isinstance(credentials_config, dict):
56
+ self._auth_manager = AgentAuthManager(config=credentials_config)
57
+ else:
58
+ self._auth_manager = AgentAuthManager()
59
+
60
+ self._build_interceptors(agent_cards)
61
+
62
+ def _build_interceptors(self, agent_cards: List[Any]):
63
+ if not self._auth_manager:
64
+ logger.info("[AuthManager] No auth manager, skipping interceptor build")
65
+ return
66
+ for card in agent_cards:
67
+ if not hasattr(card, "name"):
68
+ continue
69
+ interceptors = []
70
+ cred_svc = None
71
+ if card.security_schemes and card.security_requirements:
72
+ cred_svc = self._auth_manager.get_service(card.name)
73
+ else:
74
+ logger.info(f"[AuthManager] Agent {card.name}: no security schemes, skipping auth")
75
+ if not (getattr(card, "capabilities", None) and card.capabilities.extensions):
76
+ continue
77
+ if cred_svc is not None:
78
+ logger.info(f"[AuthManager] Agent {card.name}: credentials found")
79
+ agent_cfg = self._auth_manager.get_config(card.name) or {}
80
+ if any(isinstance(v, dict) and (v.get("auth_header") or v.get("accept_header"))
81
+ for v in agent_cfg.values()):
82
+ interceptors.append(CustomAuthInterceptor(cred_svc, agent_cfg))
83
+ else:
84
+ interceptors.append(AuthInterceptor(cred_svc))
85
+ logger.info(f"[AuthManager] Agent {card.name}: configured with {type(interceptors[0]).__name__}")
86
+ if getattr(card, "capabilities", None) and card.capabilities.extensions:
87
+ ext_uris = [ext.uri for ext in card.capabilities.extensions if ext.uri]
88
+ if ext_uris:
89
+ interceptors.append(ExtensionInterceptor(ext_uris))
90
+ if interceptors:
91
+ self._interceptors[card.name] = interceptors
92
+
93
+ def get_interceptors(self, agent_name: str) -> List[Any]:
94
+ return self._interceptors.get(agent_name, [])
95
+
96
+ def set_httpx_client(self, client):
97
+ if self._auth_manager:
98
+ self._auth_manager.set_httpx_client(client)
99
+ class AuthProviderInterceptor(ClientCallInterceptor if _A2A_AUTH_AVAILABLE else object):
100
+ """Wraps a custom AuthProvider as an a2a-sdk ClientCallInterceptor.
101
+
102
+ Calls ``auth_provider.apply_auth(agent_name, agent_card, headers)`` on
103
+ every ``before`` to inject auth headers. Mirrors the Java SDK's
104
+ ``AuthProvider.applyAuth`` being called in buildClientCallContext.
105
+ """
106
+ def __init__(self, auth_provider: AuthProvider, agent_name: str):
107
+ self._auth_provider = auth_provider
108
+ self._agent_name = agent_name
109
+
110
+ async def before(self, args: "BeforeArgs") -> None:
111
+ agent_card = args.agent_card
112
+ headers: Dict[str, str] = {}
113
+ try:
114
+ self._auth_provider.apply_auth(self._agent_name, agent_card, headers)
115
+ except Exception as e:
116
+ logger.warning(f"[AuthProvider] apply_auth raised: {e}")
117
+ if args.context is None:
118
+ from a2a.client.client import ClientCallContext
119
+ args.context = ClientCallContext()
120
+ if args.context.service_parameters is None:
121
+ args.context.service_parameters = {}
122
+ args.context.service_parameters.update(headers)
123
+ if headers:
124
+ logger.info(f"[AuthProvider] Injected {len(headers)} header(s) for {self._agent_name}")
125
+
126
+ async def after(self, args: "AfterArgs") -> None:
127
+ pass
@@ -0,0 +1,47 @@
1
+ # Copyright (c) 2026 Huawei Technologies Co., Ltd.
2
+ # All Rights Reserved.
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ """Custom authentication provider for injecting auth headers.
19
+
20
+ Implement this when the agent's authentication is not covered by the
21
+ credentials file or the AgentCard's security schemes (e.g. corporate SSO,
22
+ non-standard auth). Mirrors the Java SDK's ``AuthProvider`` interface.
23
+
24
+ Register via ``WorkflowEngineClient(..., auth_provider=my_provider)``.
25
+ The provider is called for every message send, regardless of whether the
26
+ AgentCard declares security schemes. If both a credentials config and a
27
+ custom AuthProvider are configured, both run (custom provider first,
28
+ credentials-based auth second).
29
+ """
30
+
31
+ from abc import ABC, abstractmethod
32
+ from typing import Any, Dict
33
+
34
+
35
+ class AuthProvider(ABC):
36
+ """Apply authentication headers for sending a message to an agent."""
37
+
38
+ @abstractmethod
39
+ def apply_auth(self, agent_name: str, agent_card: Any, headers: Dict[str, str]) -> None:
40
+ """Add auth headers to the mutable ``headers`` dict.
41
+
42
+ Args:
43
+ agent_name: target agent name (matches AgentCard.name)
44
+ agent_card: the agent's card (security_schemes may be empty)
45
+ headers: mutable header map to add auth headers to
46
+ """
47
+ ...
@@ -0,0 +1,102 @@
1
+ # Copyright (c) 2026 Huawei Technologies Co., Ltd.
2
+ # All Rights Reserved.
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ """AES-GCM credential encryption/decryption utility.
19
+
20
+ Supports encrypted values in credential config files using the ``enc:``
21
+ prefix. The encryption key is read from the ``A2AT_CRED_KEY`` environment
22
+ variable (32-byte hex string). Mirrors the Java SDK's ``CredentialCrypto``.
23
+
24
+ Usage in credentials JSON::
25
+
26
+ {"value": "enc:<base64-iv>:<base64-ciphertext>"}
27
+
28
+ Plaintext values (no ``enc:`` prefix) are returned as-is for backward compat.
29
+ """
30
+
31
+ import os
32
+ import base64
33
+ import secrets
34
+ from typing import Optional
35
+
36
+ from loguru import logger
37
+
38
+ _ENV_KEY = "A2AT_CRED_KEY"
39
+ _PREFIX = "enc:"
40
+ _IV_LENGTH = 12 # 96-bit IV for GCM
41
+ _TAG_LENGTH = 16 # 128-bit auth tag
42
+
43
+
44
+ def _resolve_key() -> Optional[str]:
45
+ """Resolve the encryption key from OS environment."""
46
+ key = os.environ.get(_ENV_KEY)
47
+ if key:
48
+ return key
49
+ return None
50
+
51
+
52
+ def decrypt_if_needed(value: Optional[str]) -> Optional[str]:
53
+ """Decrypt a credential value if it has the ``enc:`` prefix.
54
+
55
+ Values without the prefix are returned as-is (plaintext fallback).
56
+ """
57
+ if not value or not value.startswith(_PREFIX):
58
+ return value
59
+ key_hex = _resolve_key()
60
+ if not key_hex or not key_hex.strip():
61
+ logger.warning(
62
+ f"[CredentialCrypto] Encrypted value found but {_ENV_KEY} not set, using as-is"
63
+ )
64
+ return value
65
+ try:
66
+ encoded = value[len(_PREFIX):]
67
+ parts = encoded.split(":", 1)
68
+ if len(parts) != 2:
69
+ logger.error("[CredentialCrypto] Invalid encrypted format, expected enc:<iv>:<ciphertext>")
70
+ return value
71
+ iv = base64.b64decode(parts[0])
72
+ ciphertext_and_tag = base64.b64decode(parts[1])
73
+ key_bytes = bytes.fromhex(key_hex)
74
+ ciphertext = ciphertext_and_tag[:-_TAG_LENGTH]
75
+ tag = ciphertext_and_tag[-_TAG_LENGTH:]
76
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
77
+ plaintext = AESGCM(key_bytes).decrypt(iv, ciphertext + tag, None)
78
+ return plaintext.decode("utf-8")
79
+ except Exception as e:
80
+ logger.error(f"[CredentialCrypto] Decryption failed: {e}")
81
+ return value
82
+
83
+
84
+ def encrypt(plaintext: str) -> str:
85
+ """Encrypt a plaintext value using AES-GCM with the key from A2AT_CRED_KEY.
86
+
87
+ Returns encrypted string in format ``enc:<base64-iv>:<base64-ciphertext>``.
88
+ Raises RuntimeError if the key env var is not set.
89
+ """
90
+ key_hex = _resolve_key()
91
+ if not key_hex or not key_hex.strip():
92
+ raise RuntimeError(f"{_ENV_KEY} environment variable not set")
93
+ key_bytes = bytes.fromhex(key_hex)
94
+ iv = secrets.token_bytes(_IV_LENGTH)
95
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
96
+ ciphertext_and_tag = AESGCM(key_bytes).encrypt(iv, plaintext.encode("utf-8"), None)
97
+ return (
98
+ _PREFIX
99
+ + base64.b64encode(iv).decode("ascii")
100
+ + ":"
101
+ + base64.b64encode(ciphertext_and_tag).decode("ascii")
102
+ )
@@ -0,0 +1,229 @@
1
+ # Copyright (c) 2026 Huawei Technologies Co., Ltd.
2
+ # All Rights Reserved.
3
+ #
4
+ # SPDX-License-Identifier: Apache-2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
7
+ # not use this file except in compliance with the License. You may obtain
8
+ # a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
+ # License for the specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ """Agent credential service - self-contained, no orchestration center dependency.
19
+
20
+ Obtains Bearer tokens via login endpoints for agents requiring authentication.
21
+ Reads credentials from a user-provided config (JSON file or dict).
22
+ """
23
+
24
+ import json
25
+ import time
26
+ from pathlib import Path
27
+ from typing import Dict, Optional
28
+ import httpx
29
+ from loguru import logger
30
+
31
+ from workflow_engine.client.credential_crypto import decrypt_if_needed
32
+
33
+ try:
34
+ from a2a.client.auth import CredentialService
35
+ from a2a.client.auth import InMemoryContextCredentialStore
36
+ from a2a.client.interceptors import ClientCallInterceptor, BeforeArgs, AfterArgs
37
+ _A2A_AVAILABLE = True
38
+ except ImportError:
39
+ _A2A_AVAILABLE = False
40
+ CredentialService = object
41
+
42
+
43
+ class AgentCredentialService(CredentialService if _A2A_AVAILABLE else object):
44
+ """Obtains tokens via login endpoint, caches with TTL."""
45
+
46
+ def __init__(self, agent_name: str, scheme_configs: Dict[str, dict],
47
+ httpx_client: Optional[httpx.AsyncClient] = None):
48
+ self._agent_name = agent_name
49
+ self._schemes = scheme_configs
50
+ self._httpx_client = httpx_client
51
+ self._tokens: Dict[str, tuple] = {}
52
+ self._lock = None
53
+
54
+ def _ensure_lock(self):
55
+ if self._lock is None:
56
+ import asyncio
57
+ self._lock = asyncio.Lock()
58
+
59
+ def set_httpx_client(self, client: httpx.AsyncClient):
60
+ self._httpx_client = client
61
+
62
+ async def get_credentials(self, security_scheme_name: str, context=None) -> Optional[str]:
63
+ scheme_cfg = self._schemes.get(security_scheme_name)
64
+ if not scheme_cfg:
65
+ return None
66
+ cached = self._tokens.get(security_scheme_name)
67
+ if cached:
68
+ token, expires_at = cached
69
+ if time.time() < expires_at - 60:
70
+ logger.info(f"[Auth] Cache hit for agent {self._agent_name} scheme {security_scheme_name}")
71
+ return token
72
+ self._ensure_lock()
73
+ async with self._lock:
74
+ cached = self._tokens.get(security_scheme_name)
75
+ if cached:
76
+ token, expires_at = cached
77
+ if time.time() < expires_at - 60:
78
+ return token
79
+ token = await self._login(scheme_cfg)
80
+ if token:
81
+ ttl = scheme_cfg.get("token_ttl", 3600)
82
+ self._tokens[security_scheme_name] = (token, time.time() + ttl)
83
+ logger.info(f"[Auth] Login succeeded: agent={self._agent_name}, scheme={security_scheme_name}")
84
+ return token
85
+
86
+ async def _login(self, scheme_cfg: dict) -> Optional[str]:
87
+ login_url = scheme_cfg.get("login_url")
88
+ if not login_url:
89
+ return None
90
+ method = scheme_cfg.get("method", "POST").upper()
91
+ content_type = scheme_cfg.get("content_type", "application/json")
92
+ token_field = scheme_cfg.get("token_field", "accessSession")
93
+ request_fields = scheme_cfg.get("request_fields")
94
+ if request_fields and isinstance(request_fields, dict):
95
+ body = {k: decrypt_if_needed(v) if isinstance(v, str) else v
96
+ for k, v in request_fields.items()}
97
+ else:
98
+ username = scheme_cfg.get("username")
99
+ password = decrypt_if_needed(scheme_cfg.get("password"))
100
+ if not username or not password:
101
+ return None
102
+ body = {scheme_cfg.get("username_field","username"): username, scheme_cfg.get("password_field","password"): password}
103
+ client = self._httpx_client or httpx.AsyncClient(timeout=httpx.Timeout(connect=30, read=30, write=30, pool=5.0), verify=False)
104
+ own_client = self._httpx_client is None
105
+ try:
106
+ logger.info(f"[Auth] Login attempt: agent={self._agent_name}, method={method}, url={login_url}, content_type={content_type}, params={_sanitize_body(body)}")
107
+ req_kwargs = {"method": method, "url": login_url}
108
+ if content_type == "application/x-www-form-urlencoded":
109
+ req_kwargs["data"] = body
110
+ else:
111
+ req_kwargs["json"] = body
112
+ resp = await client.request(**req_kwargs)
113
+ resp.raise_for_status()
114
+ data = resp.json()
115
+ token = self._extract_nested_value(data, token_field) if isinstance(data, dict) else None
116
+ if not token and isinstance(data, dict):
117
+ token = data.get("accessSession") or data.get("access_session") or data.get("access_token") or data.get("token")
118
+ return token
119
+ except Exception as e:
120
+ logger.error(f"[Auth] Login failed: agent={self._agent_name}, url={login_url}, error={e}")
121
+ return None
122
+ finally:
123
+ if own_client:
124
+ await client.aclose()
125
+
126
+ @staticmethod
127
+ def _extract_nested_value(data: dict, path: str) -> Optional[str]:
128
+ if not path:
129
+ return None
130
+ current = data
131
+ for part in path.split("."):
132
+ if not isinstance(current, dict):
133
+ return None
134
+ current = current.get(part)
135
+ if current is None:
136
+ return None
137
+ return current
138
+
139
+
140
+ class AgentAuthManager:
141
+ """Loads agent credentials from config, creates per-agent CredentialService."""
142
+
143
+ def __init__(self, config: Optional[Dict[str, dict]] = None, config_path: Optional[str] = None):
144
+ self._config: Dict[str, dict] = {}
145
+ self._services: Dict[str, AgentCredentialService] = {}
146
+ if config:
147
+ self._config = config
148
+ elif config_path:
149
+ self._load_from_file(config_path)
150
+
151
+ def _load_from_file(self, path: str):
152
+ p = Path(path)
153
+ if not p.exists():
154
+ return
155
+ try:
156
+ with open(p, "r", encoding="utf-8") as f:
157
+ self._config = json.load(f)
158
+ logger.info(f"[Auth] Loaded credentials for {len(self._config)} agent(s): {list(self._config.keys())}")
159
+ except Exception as e:
160
+ logger.warning(f"[Auth] Failed to load credentials: {e}")
161
+
162
+ def get_service(self, agent_name: str) -> Optional[AgentCredentialService]:
163
+ if agent_name in self._services:
164
+ return self._services[agent_name]
165
+ agent_creds = self._config.get(agent_name)
166
+ if not agent_creds:
167
+ return None
168
+ service = AgentCredentialService(agent_name, agent_creds)
169
+ self._services[agent_name] = service
170
+ logger.info(f"[Auth] Created credential service for agent: {agent_name}")
171
+ return service
172
+
173
+ def get_config(self, agent_name: str) -> Optional[Dict[str, dict]]:
174
+ return self._config.get(agent_name)
175
+
176
+ def set_httpx_client(self, client: httpx.AsyncClient):
177
+ for svc in self._services.values():
178
+ svc.set_httpx_client(client)
179
+
180
+
181
+ class CustomAuthInterceptor(ClientCallInterceptor if _A2A_AVAILABLE else object):
182
+ """Auth interceptor supporting custom header names."""
183
+
184
+ def __init__(self, credential_service: AgentCredentialService, scheme_configs: Dict[str, dict]):
185
+ self._credential_service = credential_service
186
+ self._scheme_configs = scheme_configs
187
+
188
+ async def before(self, args: BeforeArgs) -> None:
189
+ agent_card = args.agent_card
190
+ if not agent_card.security_requirements or not agent_card.security_schemes:
191
+ return
192
+ for requirement in agent_card.security_requirements:
193
+ for scheme_name in requirement.schemes:
194
+ scheme_cfg = self._scheme_configs.get(scheme_name, {})
195
+ credential = await self._credential_service.get_credentials(scheme_name, args.context)
196
+ if not credential:
197
+ continue
198
+ if args.context is None:
199
+ from a2a.client.client import ClientCallContext
200
+ args.context = ClientCallContext()
201
+ if args.context.service_parameters is None:
202
+ args.context.service_parameters = {}
203
+ auth_header = scheme_cfg.get("auth_header")
204
+ if auth_header:
205
+ prefix = scheme_cfg.get("auth_header_prefix", "")
206
+ args.context.service_parameters[auth_header] = f"{prefix}{credential}"
207
+ logger.info(f"[CustomAuth] Set header {auth_header} for scheme {scheme_name}")
208
+ else:
209
+ args.context.service_parameters["Authorization"] = f"Bearer {credential}"
210
+ logger.info(f"[CustomAuth] Set Bearer header for scheme {scheme_name}")
211
+ accept_header = scheme_cfg.get("accept_header")
212
+ if accept_header:
213
+ args.context.service_parameters["Accept"] = accept_header
214
+ logger.info(f"[CustomAuth] Override Accept header to {accept_header} for agent {getattr(args.agent_card, chr(39)+chr(110)+chr(97)+chr(109)+chr(101)+chr(39), chr(63))}")
215
+ return
216
+
217
+ async def after(self, args: AfterArgs) -> None:
218
+ pass
219
+
220
+
221
+ def _sanitize_body(body: dict) -> dict:
222
+ """Mask sensitive fields (password, value, accessSession) for safe logging."""
223
+ sanitized = {}
224
+ for k, v in body.items():
225
+ if k.lower() in ("password", "value", "accesssession"):
226
+ sanitized[k] = "***"
227
+ else:
228
+ sanitized[k] = v
229
+ return sanitized