@@ -356,6 +356,51 @@ impl Activation<'_> {
356356 ) )
357357 }
358358
359+ fn end_revoked ( & self , detail : String ) -> Result < Flow > {
360+ let unknown = self . rt . store ( ) . write ( |tx| {
361+ let unknown = journal:: unknown_positions ( tx, & self . lease . run_id ) ?;
362+ let exit = if unknown. is_empty ( ) {
363+ Exit :: Failed {
364+ kind : "version_revoked" . into ( ) ,
365+ detail : detail. clone ( ) ,
366+ }
367+ } else {
368+ Exit :: NeedsReconcile {
369+ positions : unknown. clone ( ) ,
370+ }
371+ } ;
372+ runs:: exit ( tx, & self . lease , & exit) ?;
373+ Ok ( unknown)
374+ } ) ?;
375+ self . at ( Boundary :: Exited ) ?;
376+ if !unknown. is_empty ( ) {
377+ return Ok ( done (
378+ ActivationEnd :: NeedsReconcile { positions : unknown } ,
379+ false ,
380+ ) ) ;
381+ }
382+ Ok ( done (
383+ ActivationEnd :: Failed {
384+ kind : "version_revoked" . into ( ) ,
385+ detail,
386+ } ,
387+ false ,
388+ ) )
389+ }
390+
391+ fn finish_revoked_call ( & mut self , call : & HostCall , reason : String ) -> Result < Flow > {
392+ let ( outcome, _) = self
393+ . rt
394+ . store ( )
395+ . write ( |tx| journal:: release_next ( tx, & self . lease , & [ call. position ] ) ) ?;
396+ if let Some ( outcome) = outcome {
397+ // Commit the typed refusal before ending the run. A script catching
398+ // this reply cannot dispatch again because its worker is retired.
399+ let _ = self . send ( & ParentMessage :: Deliver ( outcome) ) ;
400+ }
401+ self . end_revoked ( reason)
402+ }
403+
359404 fn send ( & mut self , message : & ParentMessage ) -> std:: result:: Result < ( ) , ChannelError > {
360405 self . worker . send ( message)
361406 }
@@ -373,13 +418,10 @@ impl Activation<'_> {
373418 fn run ( & mut self ) -> Result < Flow > {
374419 // The install gate (`crate::gate`): the lease is held and nothing
375420 // has reached the worker or the host yet, not even a resend of a
376- // call an earlier activation left open. A revoke that lands after
377- // this check is deliberately not looked for again during this
378- // activation. Core itself refuses a revoked version's sink writes
379- // (digest items and status lines, the only way a flow reaches an
380- // agent), and the next activation asks again, so what a revoked
381- // version can still do is bounded by this one activation. The
382- // worker was handed nothing, so it stays reusable.
421+ // call an earlier activation left open. Verified pushes can record
422+ // revocation afterward; each dispatch attempt checks that durable
423+ // record without another network read. The worker was handed nothing
424+ // when this gate closes, so it stays reusable.
383425 if let Gate :: Closed ( end) = self . rt . check_install ( & self . lease , & self . run ) ? {
384426 return Ok ( done ( end, true ) ) ;
385427 }
@@ -610,7 +652,10 @@ impl Activation<'_> {
610652 // If only the row remains, recovery completes that intent.
611653 let flow_id = self . run . flow_id . clone ( ) ;
612654 let limits = self . rt . config . kv ;
613- self . rt . store ( ) . write ( |tx| {
655+ let revoked = self . rt . store ( ) . write ( |tx| {
656+ if let Some ( reason) = crate :: revocation:: dispatch_check ( tx, & run_id) ? {
657+ return Ok ( Some ( reason) ) ;
658+ }
614659 let outcome =
615660 if matches ! ( row. kind, CallKind :: Primitive ( Primitive :: InstanceEnd ) ) {
616661 crate :: templates:: request_end ( tx, & run_id, & row. args ) ?
@@ -624,8 +669,12 @@ impl Activation<'_> {
624669 None ,
625670 & outcome,
626671 Source :: Local ,
627- )
672+ ) ?;
673+ Ok ( None )
628674 } ) ?;
675+ if let Some ( reason) = revoked {
676+ return self . end_revoked ( reason) ;
677+ }
629678 self . at ( Boundary :: LocalCommitted {
630679 position : row. position ,
631680 } ) ?;
@@ -721,6 +770,36 @@ impl Activation<'_> {
721770 }
722771
723772 fn on_host_call ( & mut self , call : HostCall ) -> Result < Flow > {
773+ if let Some ( reason) = self
774+ . rt
775+ . store ( )
776+ . read ( |c| crate :: revocation:: dispatch_check ( c, & self . lease . run_id ) ) ?
777+ {
778+ if call. position == self . next_position {
779+ self . refuse (
780+ & call,
781+ & self . run . flow_id . clone ( ) ,
782+ & Refusal :: new ( "version_revoked" , & reason) ,
783+ ) ?;
784+ } else if call. position < self . next_position {
785+ self . rt . store ( ) . write ( |tx| {
786+ if journal:: row ( tx, & self . lease . run_id , call. position ) ?
787+ . is_some_and ( |r| r. class == StoredClass :: Sync && r. outcome . is_none ( ) )
788+ {
789+ journal:: accept_outcome (
790+ tx,
791+ & self . lease . run_id ,
792+ call. position ,
793+ None ,
794+ & kv:: rejection ( "version_revoked" , & reason) ,
795+ Source :: Local ,
796+ ) ?;
797+ }
798+ Ok ( ( ) )
799+ } ) ?;
800+ }
801+ return self . finish_revoked_call ( & call, reason) ;
802+ }
724803 if matches ! ( call. kind, CallKind :: Tool { .. } ) {
725804 return self . fail (
726805 "profile_violation" ,
@@ -810,6 +889,13 @@ impl Activation<'_> {
810889 position : call. position ,
811890 } ) ?;
812891 self . rt . shared . signal . bump ( ) ;
892+ if let Some ( reason) = self
893+ . rt
894+ . store ( )
895+ . read ( |c| crate :: revocation:: dispatch_check ( c, & run_id) ) ?
896+ {
897+ return self . finish_revoked_call ( & call, reason) ;
898+ }
813899 return Ok ( Flow :: Continue ) ;
814900 }
815901 Err ( e) => {
@@ -848,6 +934,10 @@ impl Activation<'_> {
848934 ) -> Result < Flow > {
849935 let limits = self . rt . config . kv ;
850936 let refused = self . rt . store ( ) . write ( |tx| {
937+ if let Some ( reason) = crate :: revocation:: dispatch_check ( tx, & self . lease . run_id ) ? {
938+ self . record_refusal ( tx, flow_id, call, & Refusal :: new ( "version_revoked" , reason) ) ?;
939+ return Ok ( true ) ;
940+ }
851941 if install:: is_disabled ( tx, flow_id) ? {
852942 self . record_refusal ( tx, flow_id, call, & disabled_refusal ( ) ) ?;
853943 return Ok ( true ) ;
@@ -891,6 +981,13 @@ impl Activation<'_> {
891981 }
892982 } ) ?;
893983 self . rt . shared . signal . bump ( ) ;
984+ if let Some ( reason) = self
985+ . rt
986+ . store ( )
987+ . read ( |c| crate :: revocation:: dispatch_check ( c, & self . lease . run_id ) ) ?
988+ {
989+ return self . finish_revoked_call ( call, reason) ;
990+ }
894991 Ok ( Flow :: Continue )
895992 }
896993
@@ -901,8 +998,12 @@ impl Activation<'_> {
901998 new : & NewCall < ' _ > ,
902999 audited_at : i64 ,
9031000 ) -> Result < Flow > {
904- let ( settlement, value, clock) = self . sync_value ( & call. kind ) ?;
9051001 let outcome = self . rt . store ( ) . write ( |tx| {
1002+ if let Some ( reason) = crate :: revocation:: dispatch_check ( tx, & self . lease . run_id ) ? {
1003+ self . record_refusal ( tx, flow_id, call, & Refusal :: new ( "version_revoked" , & reason) ) ?;
1004+ return Ok ( Err ( reason) ) ;
1005+ }
1006+ let ( settlement, value, clock) = self . sync_value ( & call. kind ) ?;
9061007 journal:: insert_call ( tx, & self . lease , flow_id, new) ?;
9071008 audit:: record (
9081009 tx,
@@ -921,9 +1022,13 @@ impl Activation<'_> {
9211022 ( c, _) => c,
9221023 } ;
9231024 let value = clock. map_or ( value. clone ( ) , number_text) ;
924- journal:: record_sync ( tx, & self . lease , call. position , settlement, & value, clock)
1025+ journal:: record_sync ( tx, & self . lease , call. position , settlement, & value, clock) . map ( Ok )
9251026 } ) ?;
9261027 self . next_position += 1 ;
1028+ let outcome = match outcome {
1029+ Ok ( outcome) => outcome,
1030+ Err ( reason) => return self . finish_revoked_call ( call, reason) ,
1031+ } ;
9271032 self . at ( Boundary :: SyncCommitted {
9281033 position : call. position ,
9291034 } ) ?;
@@ -1290,6 +1395,10 @@ impl Activation<'_> {
12901395 prepared : & Prepared ,
12911396 now_ms : i64 ,
12921397 ) -> Result < Option < String > > {
1398+ if let Some ( reason) = crate :: revocation:: dispatch_check ( tx, & self . lease . run_id ) ? {
1399+ self . record_refusal ( tx, flow_id, call, & Refusal :: new ( "version_revoked" , reason) ) ?;
1400+ return Ok ( None ) ;
1401+ }
12931402 let rate = self . rt . config . rate ;
12941403 if install:: is_disabled ( tx, flow_id) ? {
12951404 self . record_refusal ( tx, flow_id, call, & disabled_refusal ( ) ) ?;
@@ -1383,8 +1492,19 @@ impl Activation<'_> {
13831492 call. position
13841493 ) ) ;
13851494 }
1386- let ( settlement, value, clock) = self . sync_value ( & call. kind ) ?;
13871495 let outcome = self . rt . store ( ) . write ( |tx| {
1496+ if let Some ( reason) = crate :: revocation:: dispatch_check ( tx, & self . lease . run_id ) ? {
1497+ journal:: accept_outcome (
1498+ tx,
1499+ & self . lease . run_id ,
1500+ call. position ,
1501+ None ,
1502+ & kv:: rejection ( "version_revoked" , & reason) ,
1503+ Source :: Local ,
1504+ ) ?;
1505+ return Ok ( Err ( reason) ) ;
1506+ }
1507+ let ( settlement, value, clock) = self . sync_value ( & call. kind ) ?;
13881508 // Recovery must use the same monotonic clock rule as a new call.
13891509 let clock = match clock {
13901510 Some ( sample) => {
@@ -1393,8 +1513,12 @@ impl Activation<'_> {
13931513 None => None ,
13941514 } ;
13951515 let value = clock. map_or ( value. clone ( ) , number_text) ;
1396- journal:: record_sync ( tx, & self . lease , call. position , settlement, & value, clock)
1516+ journal:: record_sync ( tx, & self . lease , call. position , settlement, & value, clock) . map ( Ok )
13971517 } ) ?;
1518+ let outcome = match outcome {
1519+ Ok ( outcome) => outcome,
1520+ Err ( reason) => return self . finish_revoked_call ( & call, reason) ,
1521+ } ;
13981522 self . at ( Boundary :: SyncCommitted {
13991523 position : call. position ,
14001524 } ) ?;
@@ -1431,6 +1555,14 @@ impl Activation<'_> {
14311555 self . readiness_seen = readiness;
14321556 if let Some ( outcome) = outcome {
14331557 let position = outcome. position ;
1558+ if outcome. settlement == Settlement :: Rejected
1559+ && serde_json:: from_str :: < Value > ( outcome. value . as_str ( ) )
1560+ . ok ( )
1561+ . is_some_and ( |v| v[ "code" ] == "version_revoked" )
1562+ {
1563+ let _ = self . send ( & ParentMessage :: Deliver ( outcome) ) ;
1564+ return self . end_revoked ( "version revoked before dispatch" . into ( ) ) ;
1565+ }
14341566 self . at ( Boundary :: OrderCommitted {
14351567 position,
14361568 order : outcome. delivery_order ,
@@ -1484,6 +1616,13 @@ impl Activation<'_> {
14841616 }
14851617
14861618 fn on_finished ( & self , result : ActivationResult ) -> Result < Flow > {
1619+ if let Some ( reason) = self
1620+ . rt
1621+ . store ( )
1622+ . read ( |c| crate :: revocation:: dispatch_check ( c, & self . lease . run_id ) ) ?
1623+ {
1624+ return self . end_revoked ( reason) ;
1625+ }
14871626 match result {
14881627 ActivationResult :: Completed { value } => {
14891628 let exit = Exit :: Succeeded {
0 commit comments