@@ -157,6 +157,18 @@ fn test_identity_introspect_response(request: &str) -> (u16, &'static str) {
157157 ( !token. is_empty ( ) ) . then_some ( token)
158158 } ) ;
159159 match token {
160+ Some ( "desktop-hosted-token" ) => (
161+ 200 ,
162+ r#"{"active":true,"subject":"user-test","token_type":"access","organization_id":"org-test","workspace_id":"workspace-test","scopes":["llm_gateway:invoke","console:read","console:write"]}"# ,
163+ ) ,
164+ Some ( "desktop-readonly-token" ) => (
165+ 200 ,
166+ r#"{"active":true,"subject":"user-test","token_type":"access","organization_id":"org-test","workspace_id":"workspace-test","scopes":["llm_gateway:invoke","console:read"]}"# ,
167+ ) ,
168+ Some ( "desktop-writeonly-token" ) => (
169+ 200 ,
170+ r#"{"active":true,"subject":"user-test","token_type":"access","organization_id":"org-test","workspace_id":"workspace-test","scopes":["llm_gateway:invoke","console:write"]}"# ,
171+ ) ,
160172 Some ( "inactive-token" ) => (
161173 200 ,
162174 r#"{"active":false,"subject":"user-test","token_type":"access","organization_id":"org-test","workspace_id":"workspace-test","scopes":["llm_gateway:invoke"]}"# ,
@@ -431,7 +443,7 @@ fn current_verified_identity_session_with_env()
431443 false ,
432444 ) ;
433445 }
434- let identity = verify_live_runtime_identity ( snapshot. as_ref ( ) , & env) ?;
446+ let identity = verify_live_runtime_identity ( snapshot. as_ref ( ) , & env, & [ ] ) ?;
435447 Ok ( ( identity, env) )
436448}
437449
@@ -448,6 +460,41 @@ pub fn verified_current_identity_session() -> Result<PlatformSession> {
448460 verify_live_identity_session ( snapshot. as_ref ( ) , & env)
449461}
450462
463+ /// Verify a desktop-held access token without reading or replacing the CLI
464+ /// credential. The expected tenant is compared after live introspection.
465+ pub fn verified_desktop_identity_session (
466+ access_token : & str ,
467+ organization_id : & str ,
468+ workspace_id : & str ,
469+ ) -> Result < PlatformSession > {
470+ crate :: safety:: require_vendor_network ( ) ?;
471+ if access_token. trim ( ) . is_empty ( )
472+ || organization_id. trim ( ) . is_empty ( )
473+ || workspace_id. trim ( ) . is_empty ( )
474+ {
475+ bail ! ( "desktop Identity credential is incomplete" ) ;
476+ }
477+ let mut env = std:: env:: vars ( )
478+ . filter ( |( key, _) | {
479+ matches ! (
480+ key. as_str( ) ,
481+ "MAESTRO_IDENTITY_URL" | "EVALOPS_IDENTITY_URL"
482+ ) || key == crate :: init_cli:: TEST_IDENTITY_AUTHORITY_ENV
483+ } )
484+ . collect :: < HashMap < _ , _ > > ( ) ;
485+ env. insert ( ACCESS_TOKEN_ENV . to_owned ( ) , access_token. to_owned ( ) ) ;
486+ env. insert ( ORG_ID_ENV . to_owned ( ) , organization_id. to_owned ( ) ) ;
487+ env. insert ( WORKSPACE_ID_ENV . to_owned ( ) , workspace_id. to_owned ( ) ) ;
488+ let session = verify_live_runtime_identity ( None , & env, & [ "console:read" , "console:write" ] ) ?
489+ . require_human ( ) ?;
490+ if session. organization_id != organization_id
491+ || session. workspace_id . as_deref ( ) != Some ( workspace_id)
492+ {
493+ bail ! ( "desktop Identity tenant differs from verified token" ) ;
494+ }
495+ Ok ( session)
496+ }
497+
451498/// Return a replacement verified session only when the credential backing a
452499/// long-running capture hook changed or reached its refresh window.
453500///
@@ -595,12 +642,13 @@ fn verify_live_identity_session(
595642 snapshot : Option < & EvalOpsCredentialSnapshot > ,
596643 env : & HashMap < String , String > ,
597644) -> Result < PlatformSession > {
598- verify_live_runtime_identity ( snapshot, env) ?. require_human ( )
645+ verify_live_runtime_identity ( snapshot, env, & [ ] ) ?. require_human ( )
599646}
600647
601648fn verify_live_runtime_identity (
602649 snapshot : Option < & EvalOpsCredentialSnapshot > ,
603650 env : & HashMap < String , String > ,
651+ additional_scopes : & [ & str ] ,
604652) -> Result < VerifiedIdentity > {
605653 let Some ( unverified) = platform_session_from ( snapshot, env) else {
606654 bail ! ( "{IDENTITY_REQUIRED_MESSAGE}" ) ;
@@ -653,6 +701,16 @@ fn verify_live_runtime_identity(
653701 . join ( )
654702 . map_err ( |_| anyhow:: anyhow!( "EvalOps Identity verification thread panicked" ) ) ?
655703 . with_context ( || IDENTITY_REQUIRED_MESSAGE . to_owned ( ) ) ?;
704+ if additional_scopes. iter ( ) . any ( |required| {
705+ !introspection
706+ . scopes
707+ . iter ( )
708+ . map ( String :: as_str)
709+ . chain ( introspection. scope . split_whitespace ( ) )
710+ . any ( |present| present == * required)
711+ } ) {
712+ bail ! ( "desktop Identity token lacks required hosted thread scope" ) ;
713+ }
656714 verified_runtime_identity ( unverified, introspection, hosted)
657715}
658716
@@ -732,6 +790,7 @@ pub(crate) fn verified_platform_session_for_scope(
732790 // select a tenant different from the signed Identity token.
733791 session. organization_id = organization_id. expect ( "checked above" ) . to_owned ( ) ;
734792 session. workspace_id = workspace_id. map ( str:: to_owned) ;
793+ session. user_id = Some ( introspection. subject ) ;
735794 Ok ( session)
736795}
737796
@@ -1725,6 +1784,53 @@ mod tests {
17251784 ) ;
17261785 }
17271786
1787+ #[ test]
1788+ fn desktop_credential_is_live_verified_and_cannot_select_another_tenant ( ) {
1789+ let _guard = crate :: config:: test_process_env_lock ( ) ;
1790+ let _restore = EnvRestore :: capture ( & [
1791+ "MAESTRO_IDENTITY_URL" ,
1792+ crate :: init_cli:: TEST_IDENTITY_AUTHORITY_ENV ,
1793+ ] ) ;
1794+ std:: env:: set_var ( "MAESTRO_IDENTITY_URL" , test_identity_base_url ( ) ) ;
1795+ std:: env:: set_var ( crate :: init_cli:: TEST_IDENTITY_AUTHORITY_ENV , "1" ) ;
1796+ let session =
1797+ verified_desktop_identity_session ( "desktop-hosted-token" , "org-test" , "workspace-test" )
1798+ . expect ( "matching desktop token" ) ;
1799+ assert_eq ! ( session. user_id. as_deref( ) , Some ( "user-test" ) ) ;
1800+ assert ! (
1801+ verified_desktop_identity_session(
1802+ "desktop-hosted-token" ,
1803+ "other-org" ,
1804+ "workspace-test"
1805+ )
1806+ . is_err( )
1807+ ) ;
1808+ assert ! (
1809+ verified_desktop_identity_session(
1810+ "desktop-hosted-token" ,
1811+ "org-test" ,
1812+ "other-workspace"
1813+ )
1814+ . is_err( )
1815+ ) ;
1816+ assert ! (
1817+ verified_desktop_identity_session( "valid-token" , "org-test" , "workspace-test" ) . is_err( )
1818+ ) ;
1819+ for token in [ "desktop-readonly-token" , "desktop-writeonly-token" ] {
1820+ assert ! (
1821+ verified_desktop_identity_session( token, "org-test" , "workspace-test" ) . is_err( )
1822+ ) ;
1823+ }
1824+ assert ! (
1825+ verified_desktop_identity_session( "inactive-token" , "org-test" , "workspace-test" )
1826+ . is_err( )
1827+ ) ;
1828+ assert ! (
1829+ verified_desktop_identity_session( "unscoped-token" , "org-test" , "workspace-test" )
1830+ . is_err( )
1831+ ) ;
1832+ }
1833+
17281834 #[ test]
17291835 fn live_introspection_rejects_inactive_and_unscoped_tokens ( ) {
17301836 let _guard = crate :: config:: test_process_env_lock ( ) ;
0 commit comments