@@ -64,19 +64,26 @@ def _redact_body(body: Any) -> Any:
6464 return None
6565
6666
67- def _raise_token_endpoint_error (resp : httpx .Response , * , message_prefix : str ) -> None :
67+ def _raise_token_endpoint_error (resp : httpx .Response , * , message_prefix : str , hint : Optional [ str ] = None ) -> None :
6868 """Raise a redacted :class:`WorkloadIdentityError` from a non-200 token-endpoint response.
6969
7070 Shared between the jwt-bearer exchange path in this module and the
7171 refresh_token grant path in :mod:`_providers`.
72+
73+ ``hint`` is an optional caller-supplied diagnostic appended verbatim to the
74+ error message (after the redacted body). Callers gate it on the response
75+ status and their own state — this helper does not inspect ``resp`` for it.
7276 """
7377 try :
7478 payload : Any = resp .json ()
7579 except ValueError :
7680 payload = resp .text
7781 redacted = _redact_body (payload )
82+ message = f"{ message_prefix } (HTTP { resp .status_code } ): { redacted } "
83+ if hint :
84+ message = f"{ message } { hint } "
7885 raise WorkloadIdentityError (
79- f" { message_prefix } (HTTP { resp . status_code } ): { redacted } " ,
86+ message ,
8087 status_code = resp .status_code ,
8188 body = redacted ,
8289 request_id = _request_id (resp ),
@@ -128,6 +135,16 @@ class WorkloadIdentityCredentials:
128135 Args:
129136 organization_id: The organization's raw UUID string (organizations do
130137 not use tagged IDs).
138+ workspace_id: Optional ``wrkspc_*`` tagged ID, or the literal
139+ ``"default"`` to scope the token to the organization's default
140+ workspace. When omitted the server picks the rule's sole enabled
141+ workspace, else the org default if the rule covers it. Required
142+ when the rule enables more than one non-default workspace, or to
143+ target a specific workspace other than the one the server would
144+ pick. The minted token is workspace-scoped: per-request workspace
145+ selection (the ``anthropic-workspace-id`` header) is not supported
146+ for federation tokens — switching workspaces requires a new token
147+ exchange with a different ``workspace_id``.
131148 """
132149
133150 def __init__ (
@@ -137,13 +154,15 @@ def __init__(
137154 federation_rule_id : str ,
138155 organization_id : str ,
139156 service_account_id : Optional [str ] = None ,
157+ workspace_id : Optional [str ] = None ,
140158 scope : Optional [str ] = None ,
141159 http_client : Optional [httpx .Client ] = None ,
142160 ) -> None :
143161 self ._identity_token_provider = identity_token_provider
144162 self ._federation_rule_id = federation_rule_id
145163 self ._organization_id = organization_id
146164 self ._service_account_id = service_account_id
165+ self ._workspace_id = workspace_id
147166 # Scope is informational only for federation: the server derives the
148167 # effective scope from the matching federation rule and the gateway
149168 # transform drops unknown body fields, so it is intentionally NOT sent
@@ -220,6 +239,8 @@ def __call__(self, *, force_refresh: bool = False) -> AccessToken:
220239 }
221240 if self ._service_account_id is not None :
222241 body ["service_account_id" ] = self ._service_account_id
242+ if self ._workspace_id is not None :
243+ body ["workspace_id" ] = self ._workspace_id
223244
224245 url = f"{ self ._base_url } { TOKEN_ENDPOINT } "
225246 try :
@@ -246,7 +267,21 @@ def __call__(self, *, force_refresh: bool = False) -> AccessToken:
246267 )
247268
248269 if resp .status_code >= 400 :
249- _raise_token_endpoint_error (resp , message_prefix = "Token exchange failed" )
270+ # A 401 is almost always a federation-rule mismatch. Point at the
271+ # rule and the Console auth-event log; when the caller hasn't pinned
272+ # a workspace, also surface the multi-workspace fix rather than
273+ # making them dig through docs.
274+ hint : Optional [str ] = None
275+ if resp .status_code == 401 :
276+ hint = "Ensure your federation rule matches your identity token. "
277+ if self ._workspace_id is None :
278+ hint += (
279+ "If your federation rule is scoped to multiple workspaces, set the "
280+ "ANTHROPIC_WORKSPACE_ID environment variable, the 'workspace_id' "
281+ "config key, or the workspace_id= argument. "
282+ )
283+ hint += "View your authentication events in the Workload identity page of Claude Console for more details."
284+ _raise_token_endpoint_error (resp , message_prefix = "Token exchange failed" , hint = hint )
250285
251286 try :
252287 data = resp .json ()
@@ -289,6 +324,7 @@ def exchange_federation_assertion(
289324 federation_rule_id : str ,
290325 organization_id : str ,
291326 service_account_id : Optional [str ] = None ,
327+ workspace_id : Optional [str ] = None ,
292328 base_url : Optional [str ] = None ,
293329 http_client : Optional [httpx .Client ] = None ,
294330) -> AccessToken :
@@ -304,6 +340,7 @@ def exchange_federation_assertion(
304340 federation_rule_id = federation_rule_id ,
305341 organization_id = organization_id ,
306342 service_account_id = service_account_id ,
343+ workspace_id = workspace_id ,
307344 http_client = http_client ,
308345 )
309346 if base_url is not None :
0 commit comments