@@ -3,11 +3,33 @@ use std::collections::BTreeMap;
33use rexos:: config:: { ProviderConfig , ProviderKind , RexosConfig , RouteConfig , RouterConfig } ;
44use rexos:: paths:: RexosPaths ;
55use rexos:: router:: TaskKind ;
6- use rexos:: security:: SecurityConfig ;
6+ use rexos:: security:: { LeakMode , SecurityConfig } ;
77use serde_json:: { json, Value } ;
88
99mod support;
1010
11+ struct EnvVarGuard {
12+ key : & ' static str ,
13+ prev : Option < String > ,
14+ }
15+
16+ impl EnvVarGuard {
17+ fn set ( key : & ' static str , value : & str ) -> Self {
18+ let prev = std:: env:: var ( key) . ok ( ) ;
19+ std:: env:: set_var ( key, value) ;
20+ Self { key, prev }
21+ }
22+ }
23+
24+ impl Drop for EnvVarGuard {
25+ fn drop ( & mut self ) {
26+ match self . prev . as_ref ( ) {
27+ Some ( value) => std:: env:: set_var ( self . key , value) ,
28+ None => std:: env:: remove_var ( self . key ) ,
29+ }
30+ }
31+ }
32+
1133fn fixture_agent (
1234 tmp : & tempfile:: TempDir ,
1335 fixture_base_url : String ,
@@ -476,3 +498,128 @@ async fn replay_fixture_executes_mcp_tool_calls() {
476498
477499 server. abort ( ) ;
478500}
501+
502+ #[ tokio:: test]
503+ async fn replay_fixture_enforces_tool_approval_for_dangerous_tools ( ) {
504+ let _mode = EnvVarGuard :: set ( "LOOPFORGE_APPROVAL_MODE" , "enforce" ) ;
505+ let _allow = EnvVarGuard :: set ( "LOOPFORGE_APPROVAL_ALLOW" , "" ) ;
506+
507+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
508+ "fixtures/replay/session_tool_approval_required.json"
509+ ) ) ;
510+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
511+
512+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
513+ let ( agent, _paths, workspace_root) = fixture_agent (
514+ & tmp,
515+ server. base_url . clone ( ) ,
516+ rexos:: security:: SecurityConfig :: default ( ) ,
517+ ) ;
518+
519+ let session_id = "s-replay-approval" ;
520+ agent
521+ . set_session_allowed_tools ( session_id, vec ! [ "shell" . to_string( ) ] )
522+ . unwrap ( ) ;
523+
524+ let err = agent
525+ . run_session (
526+ workspace_root,
527+ session_id,
528+ None ,
529+ "run shell" ,
530+ TaskKind :: Coding ,
531+ )
532+ . await
533+ . unwrap_err ( ) ;
534+ let err_text = err. to_string ( ) ;
535+ assert ! (
536+ err_text. contains( "approval required for dangerous tool `shell`" ) ,
537+ "expected approval error, got: {err_text}"
538+ ) ;
539+
540+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
541+ assert_eq ! ( requests. len( ) , 1 , "expected one chat completions call" ) ;
542+ assert_eq ! (
543+ compact_request( & requests[ 0 ] ) ,
544+ json!( {
545+ "model" : "fixture-model" ,
546+ "temperature" : 0.0 ,
547+ "tools" : [ {
548+ "name" : "shell" ,
549+ "type" : "function" ,
550+ "param_type" : "object" ,
551+ "required" : [ "command" ] ,
552+ "properties" : [ "command" , "timeout_ms" ] ,
553+ "additional_properties" : false ,
554+ } ] ,
555+ "message_roles" : [ "user" ] ,
556+ "assistant_tool_calls" : [ ] ,
557+ "tool_messages" : [ ] ,
558+ } )
559+ ) ;
560+
561+ server. abort ( ) ;
562+ }
563+
564+ #[ tokio:: test]
565+ async fn replay_fixture_blocks_leak_guard_in_enforce_mode ( ) {
566+ let fixture = support:: openai_compat_fixture:: load_json_array ( include_str ! (
567+ "fixtures/replay/session_leak_guard_enforce.json"
568+ ) ) ;
569+ let server = support:: openai_compat_fixture:: FixtureServer :: spawn ( fixture) . await ;
570+
571+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
572+ let mut security = rexos:: security:: SecurityConfig :: default ( ) ;
573+ security. leaks . mode = LeakMode :: Enforce ;
574+
575+ let ( agent, _paths, workspace_root) = fixture_agent ( & tmp, server. base_url . clone ( ) , security) ;
576+ std:: fs:: write (
577+ workspace_root. join ( "secret.txt" ) ,
578+ "secret=sk-01234567890123456789" ,
579+ )
580+ . unwrap ( ) ;
581+
582+ let session_id = "s-replay-leak-guard" ;
583+ agent
584+ . set_session_allowed_tools ( session_id, vec ! [ "fs_read" . to_string( ) ] )
585+ . unwrap ( ) ;
586+
587+ let err = agent
588+ . run_session (
589+ workspace_root,
590+ session_id,
591+ None ,
592+ "read secret" ,
593+ TaskKind :: Coding ,
594+ )
595+ . await
596+ . unwrap_err ( ) ;
597+ let err_text = err. to_string ( ) ;
598+ assert ! (
599+ err_text. contains( "tool output blocked by leak guard" ) ,
600+ "expected leak guard error, got: {err_text}"
601+ ) ;
602+
603+ let requests = server. requests . lock ( ) . unwrap ( ) . clone ( ) ;
604+ assert_eq ! ( requests. len( ) , 1 , "expected one chat completions call" ) ;
605+ assert_eq ! (
606+ compact_request( & requests[ 0 ] ) ,
607+ json!( {
608+ "model" : "fixture-model" ,
609+ "temperature" : 0.0 ,
610+ "tools" : [ {
611+ "name" : "fs_read" ,
612+ "type" : "function" ,
613+ "param_type" : "object" ,
614+ "required" : [ "path" ] ,
615+ "properties" : [ "path" ] ,
616+ "additional_properties" : false ,
617+ } ] ,
618+ "message_roles" : [ "user" ] ,
619+ "assistant_tool_calls" : [ ] ,
620+ "tool_messages" : [ ] ,
621+ } )
622+ ) ;
623+
624+ server. abort ( ) ;
625+ }
0 commit comments