@@ -662,4 +662,102 @@ describe("credential confidentiality in job.subscribed (§14)", () => {
662662 await bobClient . close ( ) ;
663663 await server . close ( ) ;
664664 } ) ;
665+
666+ it ( "observer receives budget + lease_constraints on job.subscribed (§7.6)" , async ( ) => {
667+ const server = new ARCPServer ( {
668+ runtime : TEST_RUNTIME ,
669+ capabilities : TEST_CAPABILITIES ,
670+ bearer : new StaticBearerVerifier (
671+ new Map ( [
672+ [ "tok-alice" , { principal : "alice" } ] ,
673+ [ "tok-bob" , { principal : "bob" } ] ,
674+ ] ) ,
675+ ) ,
676+ // Cross-principal subscription so bob can observe alice's job.
677+ jobAuthorizationPolicy : ( ) => true ,
678+ logger : silentLogger ,
679+ } ) ;
680+ server . registerAgent ( "slow-noop" , async ( ) => {
681+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
682+ return null ;
683+ } ) ;
684+
685+ const [ aliceClient , aliceServerSide ] = pairMemoryTransports ( ) ;
686+ const [ bobClient , bobServerSide ] = pairMemoryTransports ( ) ;
687+ server . accept ( aliceServerSide ) ;
688+ server . accept ( bobServerSide ) ;
689+ const aliceCollector = new FrameCollector ( aliceClient ) ;
690+ const bobCollector = new FrameCollector ( bobClient ) ;
691+
692+ // Alice negotiates cost.budget + lease_expires_at so the runtime
693+ // initializes/echoes those bounds.
694+ await aliceClient . send ( {
695+ arcp : PROTOCOL_VERSION ,
696+ id : "msg-hello" ,
697+ type : "session.hello" ,
698+ payload : {
699+ client : { name : "test-client" , version : "0.0.1" } ,
700+ capabilities : {
701+ encodings : [ "json" ] ,
702+ features : [ "subscribe" , "cost.budget" , "lease_expires_at" ] ,
703+ } ,
704+ auth : { scheme : "bearer" , token : "tok-alice" } ,
705+ } ,
706+ } ) ;
707+ const aliceSessionId = (
708+ await aliceCollector . waitFor ( ( f ) => f [ "type" ] === "session.welcome" )
709+ ) . find ( ( f ) => f [ "type" ] === "session.welcome" ) ! [ "session_id" ] as string ;
710+
711+ const expiresAt = new Date ( Date . now ( ) + 15 * 60_000 ) . toISOString ( ) ;
712+ await aliceClient . send ( {
713+ arcp : PROTOCOL_VERSION ,
714+ id : "msg-submit-budget" ,
715+ type : "job.submit" ,
716+ session_id : aliceSessionId ,
717+ payload : {
718+ agent : "slow-noop" ,
719+ input : { } ,
720+ lease_request : { "cost.budget" : [ "USD:5.00" ] } ,
721+ lease_constraints : { expires_at : expiresAt } ,
722+ } ,
723+ } ) ;
724+ const jobId = (
725+ ( await aliceCollector . waitFor ( ( f ) => f [ "type" ] === "job.accepted" ) ) . find (
726+ ( f ) => f [ "type" ] === "job.accepted" ,
727+ ) ! [ "payload" ] as Record < string , unknown >
728+ ) [ "job_id" ] as string ;
729+
730+ await bobClient . send ( helloFrame ( "tok-bob" ) ) ;
731+ const bobSessionId = (
732+ await bobCollector . waitFor ( ( f ) => f [ "type" ] === "session.welcome" )
733+ ) . find ( ( f ) => f [ "type" ] === "session.welcome" ) ! [ "session_id" ] as string ;
734+
735+ await bobClient . send ( {
736+ arcp : PROTOCOL_VERSION ,
737+ id : "msg-sub-budget" ,
738+ type : "job.subscribe" ,
739+ session_id : bobSessionId ,
740+ payload : { job_id : jobId } ,
741+ } ) ;
742+ const payload = (
743+ await bobCollector . waitFor ( ( f ) => f [ "type" ] === "job.subscribed" )
744+ ) . find ( ( f ) => f [ "type" ] === "job.subscribed" ) ! [ "payload" ] as Record <
745+ string ,
746+ unknown
747+ > ;
748+
749+ // Observer (bob) gets the non-secret authority bounds...
750+ expect ( payload [ "budget" ] ) . toEqual ( { USD : 5 } ) ;
751+ expect (
752+ ( payload [ "lease_constraints" ] as Record < string , unknown > | undefined ) ?. [
753+ "expires_at"
754+ ] ,
755+ ) . toBe ( expiresAt ) ;
756+ // ...but never credentials.
757+ expect ( payload [ "credentials" ] ) . toBeUndefined ( ) ;
758+
759+ await aliceClient . close ( ) ;
760+ await bobClient . close ( ) ;
761+ await server . close ( ) ;
762+ } ) ;
665763} ) ;
0 commit comments