1111//! so ready sooner — is authoritative. High-risk events thus get the deep look without paying the
1212//! serial L2+L3 latency.
1313
14- use crate :: event:: ObservedEvent ;
14+ use crate :: event:: { Event , ObservedEvent } ;
1515use crate :: verdict:: { Decision , Severity , Tier , Verdict } ;
1616use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
1717use std:: sync:: Arc ;
@@ -36,6 +36,9 @@ pub struct Pipeline {
3636 l1 : Arc < dyn Judge > ,
3737 l2 : Option < Arc < dyn Judge > > ,
3838 l3 : Option < Arc < dyn Judge > > ,
39+ /// The SAE mechanistic-interpretability tier — judges model-output `LlmActivations` events from
40+ /// a3s-power's in-enclave feature emission (a separate domain from the observer-event rule chain).
41+ sae : Option < Arc < dyn Judge > > ,
3942 fail_closed : bool ,
4043 speculate_above : Option < Severity > ,
4144 /// Live count of speculative L3 threads (incl. ones detached after an L2 short-circuit), so we
@@ -50,6 +53,7 @@ impl Pipeline {
5053 l1,
5154 l2 : None ,
5255 l3 : None ,
56+ sae : None ,
5357 fail_closed : false ,
5458 speculate_above : None ,
5559 l3_inflight : Arc :: new ( AtomicU64 :: new ( 0 ) ) ,
@@ -73,6 +77,13 @@ impl Pipeline {
7377 self
7478 }
7579
80+ /// The SAE mechanistic-interpretability tier. Model-output `LlmActivations` events route here
81+ /// instead of the L1 rule chain; an SAE escalation can still defer to the deep L3 agent.
82+ pub fn with_sae ( mut self , sae : Arc < dyn Judge > ) -> Self {
83+ self . sae = Some ( sae) ;
84+ self
85+ }
86+
7687 /// Treat an unresolved escalation (no deeper tier available) as `Block` instead of `Allow`.
7788 pub fn fail_closed ( mut self , yes : bool ) -> Self {
7889 self . fail_closed = yes;
@@ -86,8 +97,26 @@ impl Pipeline {
8697 self
8798 }
8899
100+ /// Route a model-output `LlmActivations` event to the SAE tier (with L3-on-escalate). Returns
101+ /// `None` for non-activation events, or when no SAE tier is configured (they take the rule chain).
102+ fn judge_model_output ( & self , ev : & ObservedEvent ) -> Option < Decision > {
103+ if !matches ! ( ev. event, Event :: LlmActivations { .. } ) {
104+ return None ;
105+ }
106+ let sae = self . sae . as_ref ( ) ?;
107+ let d = sae. judge ( ev) ;
108+ Some ( match ( d. verdict , & self . l3 ) {
109+ ( Verdict :: Escalate , Some ( l3) ) => l3. judge ( ev) ,
110+ ( Verdict :: Escalate , None ) => self . resolve_unescalated ( d) ,
111+ _ => d,
112+ } )
113+ }
114+
89115 /// Run the event through the tiers and return the deciding [`Decision`].
90116 pub fn evaluate ( & self , ev : & ObservedEvent ) -> Decision {
117+ if let Some ( d) = self . judge_model_output ( ev) {
118+ return d;
119+ }
91120 let d1 = self . l1 . judge ( ev) ;
92121 if d1. verdict != Verdict :: Escalate {
93122 return d1;
@@ -106,6 +135,12 @@ impl Pipeline {
106135 /// blocks the event stream. The worker then calls [`evaluate`](Pipeline::evaluate) (L1 re-runs in
107136 /// µs, then L2/L3) on the escalated event.
108137 pub fn classify_l1 ( & self , ev : & ObservedEvent ) -> Decision {
138+ // Model-output activations are judged by the SAE tier, not the rule engine.
139+ if let Some ( sae) = & self . sae {
140+ if matches ! ( ev. event, Event :: LlmActivations { .. } ) {
141+ return sae. judge ( ev) ;
142+ }
143+ }
109144 self . l1 . judge ( ev)
110145 }
111146
@@ -237,6 +272,34 @@ mod tests {
237272 assert_eq ! ( p. evaluate( & ev( ) ) . verdict, Verdict :: Block ) ;
238273 }
239274
275+ #[ test]
276+ fn model_activations_route_to_sae_tier ( ) {
277+ use crate :: event:: { Event , Identity } ;
278+ let act = ObservedEvent {
279+ identity : Identity :: default ( ) ,
280+ provider : None ,
281+ event : Event :: LlmActivations {
282+ pid : 1 ,
283+ layer : 18 ,
284+ features : vec ! [ ] ,
285+ } ,
286+ raw : String :: new ( ) ,
287+ } ;
288+ // Activations are judged by the SAE tier, not the rule chain.
289+ let p = Pipeline :: new ( Arc :: new ( Fixed ( Tier :: Rules , Verdict :: Allow ) ) )
290+ . with_sae ( Arc :: new ( Fixed ( Tier :: Sae , Verdict :: Block ) ) ) ;
291+ let d = p. evaluate ( & act) ;
292+ assert_eq ! ( d. verdict, Verdict :: Block ) ;
293+ assert_eq ! ( d. tier, Tier :: Sae ) ;
294+ // A non-activation event still takes the L1 rule chain.
295+ assert_eq ! ( p. evaluate( & ev( ) ) . verdict, Verdict :: Allow ) ;
296+ // An SAE escalation defers to the deep L3 agent when present.
297+ let p2 = Pipeline :: new ( Arc :: new ( Fixed ( Tier :: Rules , Verdict :: Allow ) ) )
298+ . with_sae ( Arc :: new ( Fixed ( Tier :: Sae , Verdict :: Escalate ) ) )
299+ . with_l3 ( Arc :: new ( Fixed ( Tier :: Agent , Verdict :: Block ) ) ) ;
300+ assert_eq ! ( p2. evaluate( & act) . tier, Tier :: Agent ) ;
301+ }
302+
240303 #[ test]
241304 fn escalates_l1_to_l2_to_l3 ( ) {
242305 let p = Pipeline :: new ( Arc :: new ( Fixed ( Tier :: Rules , Verdict :: Escalate ) ) )
0 commit comments