5151# compute environment (AgentCore runtime env / ECS container overrides).
5252SESSION_ROLE_ARN_ENV = "AGENT_SESSION_ROLE_ARN"
5353
54- # Env vars carrying the session-tag values. The agent exports these from its
55- # resolved TaskConfig at startup (see ``configure_session``) so that lazily
56- # created clients downstream can pick them up without threading config through
57- # every call site.
58- USER_ID_ENV = "AGENT_SESSION_USER_ID"
59- REPO_ENV = "AGENT_SESSION_REPO"
60- TASK_ID_ENV = "AGENT_SESSION_TASK_ID"
61-
6254# Role chaining caps the assumed session at 1 hour. Request the maximum the
6355# cap allows; botocore refreshes well before this elapses.
6456_CHAINED_SESSION_DURATION_S = 3600
7062_session : Any = None # cached boto3.Session (scoped or plain)
7163_scoped : bool | None = None # None until first resolution; True if tag-scoped
7264
65+ # Session-tag values, set once at startup by ``configure_session`` from the
66+ # resolved TaskConfig. Kept in private module state — NOT os.environ — so the
67+ # tenant identifiers are not inherited by the untrusted repo subprocesses the
68+ # agent spawns (build/test/tooling). Read by ``_session_tags`` at assume time.
69+ _tags : dict [str , str ] = {}
70+
7371
7472class SessionScopingError (RuntimeError ):
7573 """Per-session IAM scoping was requested but could not be established.
@@ -81,41 +79,40 @@ class SessionScopingError(RuntimeError):
8179
8280
8381def configure_session (user_id : str , repo : str , task_id : str ) -> None :
84- """Export session-tag values to the environment for later client creation .
82+ """Record session-tag values in private module state for later use .
8583
8684 Called once at agent startup from the resolved ``TaskConfig``. Idempotent;
8785 safe to call before any tenant-data client is created. Does not itself
8886 assume the role — assumption is deferred until the first client is built so
89- that a missing SessionRole never delays startup.
87+ that a missing SessionRole never delays startup. Values are stored in a
88+ module global (not ``os.environ``) so tenant identifiers do not leak into
89+ spawned subprocesses.
9090 """
91- if user_id :
92- os . environ [ USER_ID_ENV ] = user_id
93- if repo :
94- os . environ [ REPO_ENV ] = repo
95- if task_id :
96- os . environ [ TASK_ID_ENV ] = task_id
91+ global _tags
92+ _tags = {
93+ key : value
94+ for key , value in (( "user_id" , user_id ), ( " repo" , repo ), ( "task_id" , task_id ))
95+ if value
96+ }
9797
9898
9999def reset_session_cache () -> None :
100- """Drop the cached session. For tests that toggle env between cases ."""
101- global _session , _scoped
100+ """Drop the cached session and tags . For tests that toggle config ."""
101+ global _session , _scoped , _tags
102102 with _lock :
103103 _session = None
104104 _scoped = None
105+ _tags = {}
105106
106107
107108def _session_tags () -> list [dict [str , str ]]:
108- """Build the AssumeRole ``Tags`` list from the configured env values.
109+ """Build the AssumeRole ``Tags`` list from the configured tag values.
109110
110- Only non-empty values are included. Values are truncated to the IAM limit
111- so an over-long repo slug can never make ``AssumeRole`` fail closed.
111+ Only non-empty values are included (filtered at ``configure_session``).
112+ Values are truncated to the IAM limit so an over-long repo slug can never
113+ make ``AssumeRole`` fail closed.
112114 """
113- pairs = (
114- ("user_id" , os .environ .get (USER_ID_ENV , "" )),
115- ("repo" , os .environ .get (REPO_ENV , "" )),
116- ("task_id" , os .environ .get (TASK_ID_ENV , "" )),
117- )
118- return [{"Key" : key , "Value" : value [:_MAX_TAG_VALUE_LEN ]} for key , value in pairs if value ]
115+ return [{"Key" : key , "Value" : value [:_MAX_TAG_VALUE_LEN ]} for key , value in _tags .items ()]
119116
120117
121118def _build_scoped_session (role_arn : str ) -> Any :
@@ -132,7 +129,7 @@ def _build_scoped_session(role_arn: str) -> Any:
132129 from botocore .session import get_session as get_botocore_session
133130
134131 region = os .environ .get ("AWS_REGION" ) or os .environ .get ("AWS_DEFAULT_REGION" )
135- task_id = os . environ . get (TASK_ID_ENV , "" )
132+ task_id = _tags . get ("task_id" , "" )
136133 # Role session name must be <=64 chars and match [\w+=,.@-]. task_id is a
137134 # short slug (a ULID, ~26 chars, in the API path; a 12-char hex fallback
138135 # when the agent generates its own) — well under 64. The ``abca-`` prefix
@@ -204,7 +201,7 @@ def get_session() -> Any:
204201 f"scoped session could not be built ({ type (exc ).__name__ } : { exc } ). "
205202 "Failing closed — refusing to run on unscoped ambient credentials, "
206203 "which would disable tenant isolation." ,
207- task_id = os . environ . get (TASK_ID_ENV ) or None ,
204+ task_id = _tags . get ("task_id" ) or None ,
208205 )
209206 raise SessionScopingError (
210207 "per-session IAM scoping requested via "
0 commit comments