@@ -94,15 +94,23 @@ def configure_session(user_id: str, repo: str, task_id: str) -> None:
9494 for key , value in (("user_id" , user_id ), ("repo" , repo ), ("task_id" , task_id ))
9595 if value
9696 }
97+ # The task id doubles as the UA trace handle (#319): every AWS call made
98+ # while this task runs carries md/...#agent#{task_id}.
99+ import ua
100+
101+ ua .set_trace (task_id or None )
97102
98103
99104def reset_session_cache () -> None :
100- """Drop the cached session and tags . For tests that toggle config."""
105+ """Drop the cached session, tags, and UA trace . For tests that toggle config."""
101106 global _session , _scoped , _tags
102107 with _lock :
103108 _session = None
104109 _scoped = None
105110 _tags = {}
111+ import ua
112+
113+ ua .set_trace (None )
106114
107115
108116def _session_tags () -> list [dict [str , str ]]:
@@ -128,6 +136,8 @@ def _build_scoped_session(role_arn: str) -> Any:
128136 )
129137 from botocore .session import get_session as get_botocore_session
130138
139+ import ua
140+
131141 region = os .environ .get ("AWS_REGION" ) or os .environ .get ("AWS_DEFAULT_REGION" )
132142 task_id = _tags .get ("task_id" , "" )
133143 # Role session name must be <=64 chars and match [\w+=,.@-]. task_id is a
@@ -139,7 +149,8 @@ def _build_scoped_session(role_arn: str) -> Any:
139149 # A dedicated STS client built from the *ambient* (compute-role) chain.
140150 # This is the role-chaining caller; the assumed SessionRole credentials it
141151 # returns must NOT be used to build it, or refresh would recurse.
142- sts_client = boto3 .client ("sts" , region_name = region )
152+ sts_client = boto3 .client ("sts" , region_name = region , config = ua .client_config ())
153+ ua .register_trace_appender (sts_client .meta .events )
143154
144155 def _refresh () -> dict [str , str ]:
145156 resp = sts_client .assume_role (
@@ -167,6 +178,12 @@ def _refresh() -> dict[str, str]:
167178 )
168179 if region :
169180 botocore_session .set_config_variable ("region" , region )
181+ # Outbound UA solution tracking (#319): session-level so every client and
182+ # resource derived from this singleton carries the static segments; the
183+ # per-request #{TRACE} appender mutates only the header, preserving the
184+ # session's connection pool across trace changes.
185+ botocore_session .user_agent_extra = ua .static_user_agent_extra ()
186+ ua .register_trace_appender (botocore_session .get_component ("event_emitter" ))
170187 return boto3 .Session (botocore_session = botocore_session )
171188
172189
@@ -209,10 +226,19 @@ def get_session() -> Any:
209226 ) from exc
210227 else :
211228 # Scoping not requested (local/dev/tests, or pre-provisioning):
212- # plain ambient session, behaviorally identical to pre-feature code.
213- _session = boto3 .Session (
214- region_name = os .environ .get ("AWS_REGION" ) or os .environ .get ("AWS_DEFAULT_REGION" )
215- )
229+ # plain ambient session, behaviorally identical to pre-feature code
230+ # apart from the UA solution-tracking segments (#319).
231+ from botocore .session import get_session as get_botocore_session
232+
233+ import ua
234+
235+ botocore_session = get_botocore_session ()
236+ region = os .environ .get ("AWS_REGION" ) or os .environ .get ("AWS_DEFAULT_REGION" )
237+ if region :
238+ botocore_session .set_config_variable ("region" , region )
239+ botocore_session .user_agent_extra = ua .static_user_agent_extra ()
240+ ua .register_trace_appender (botocore_session .get_component ("event_emitter" ))
241+ _session = boto3 .Session (botocore_session = botocore_session )
216242 _scoped = False
217243 return _session
218244
@@ -235,9 +261,7 @@ def tenant_client(service_name: str, **kwargs: Any) -> Any:
235261 session = get_session ()
236262 if is_scoped ():
237263 return session .client (service_name , ** kwargs )
238- import boto3
239-
240- return boto3 .client (service_name , ** kwargs )
264+ return platform_client (service_name , ** kwargs )
241265
242266
243267def tenant_resource (service_name : str , ** kwargs : Any ) -> Any :
@@ -247,4 +271,43 @@ def tenant_resource(service_name: str, **kwargs: Any) -> Any:
247271 return session .resource (service_name , ** kwargs )
248272 import boto3
249273
250- return boto3 .resource (service_name , ** kwargs )
274+ resource = boto3 .resource (service_name , ** _with_ua (kwargs ))
275+ # Guarded like platform_client: test doubles may lack the meta chain.
276+ inner = getattr (getattr (resource , "meta" , None ), "client" , None )
277+ events = getattr (getattr (inner , "meta" , None ), "events" , None )
278+ if events is not None :
279+ import ua
280+
281+ ua .register_trace_appender (events )
282+ return resource
283+
284+
285+ def platform_client (service_name : str , ** kwargs : Any ) -> Any :
286+ """boto3 client for platform (non-tenant) calls, with the ABCA UA (#319).
287+
288+ For call sites that intentionally use the ambient compute-role chain
289+ (CloudWatch Logs debug writers, Secrets Manager, AgentCore memory) rather
290+ than the tenant-scoped session. Same signature as ``boto3.client`` plus
291+ the solution-tracking User-Agent and per-request trace appender.
292+ """
293+ import boto3
294+
295+ client = boto3 .client (service_name , ** _with_ua (kwargs ))
296+ # Real clients always expose meta.events; test doubles (MagicMock, or the
297+ # bare fakes some suites install as a stub boto3 module) may not — the
298+ # appender is solution telemetry, never worth failing a call site over.
299+ events = getattr (getattr (client , "meta" , None ), "events" , None )
300+ if events is not None :
301+ import ua
302+
303+ ua .register_trace_appender (events )
304+ return client
305+
306+
307+ def _with_ua (kwargs : dict [str , Any ]) -> dict [str , Any ]:
308+ """Merge the ABCA UA config into a boto3 client/resource kwargs dict."""
309+ import ua
310+
311+ supplied = kwargs .get ("config" )
312+ config = supplied .merge (ua .client_config ()) if supplied is not None else ua .client_config ()
313+ return {** kwargs , "config" : config }
0 commit comments