55//! recent assistant context.
66//! 2. Ask a dedicated guardian subagent to assess the exact planned action and
77//! return strict JSON.
8+ //! The guardian clones the parent config, so it inherits any managed
9+ //! network proxy / allowlist that the parent turn already had.
810//! 3. Fail closed on timeout, execution failure, or malformed output.
911//! 4. Approve only low- and medium-risk actions (`risk_score < 80`).
1012
@@ -30,6 +32,7 @@ use crate::codex_delegate::run_codex_thread_one_shot;
3032use crate :: compact:: content_items_to_text;
3133use crate :: config:: Config ;
3234use crate :: config:: Constrained ;
35+ use crate :: config:: NetworkProxySpec ;
3336use crate :: event_mapping:: is_contextual_user_message_content;
3437use crate :: features:: Feature ;
3538use crate :: protocol:: SandboxPolicy ;
@@ -383,16 +386,12 @@ async fn run_guardian_subagent(
383386 prompt : String ,
384387 schema : Value ,
385388) -> anyhow:: Result < GuardianAssessment > {
386- let mut guardian_config: Config = turn. config . as_ref ( ) . clone ( ) ;
387- guardian_config. model = Some ( GUARDIAN_MODEL . to_string ( ) ) ;
388- guardian_config. model_reasoning_effort =
389- Some ( codex_protocol:: openai_models:: ReasoningEffort :: Low ) ;
390- guardian_config. permissions . approval_policy = Constrained :: allow_only ( AskForApproval :: Never ) ;
391- guardian_config. permissions . sandbox_policy =
392- Constrained :: allow_only ( SandboxPolicy :: new_read_only_policy ( ) ) ;
393- let _ = guardian_config. features . disable ( Feature :: Collab ) ;
394- let _ = guardian_config. features . disable ( Feature :: WebSearchRequest ) ;
395- let _ = guardian_config. features . disable ( Feature :: WebSearchCached ) ;
389+ let live_network_config = match session. services . network_proxy . as_ref ( ) {
390+ Some ( network_proxy) => Some ( network_proxy. proxy ( ) . current_cfg ( ) . await ?) ,
391+ None => None ,
392+ } ;
393+ let guardian_config =
394+ build_guardian_subagent_config ( turn. config . as_ref ( ) , live_network_config) ?;
396395
397396 // `run_codex_thread_one_shot` is already the subagent runner used elsewhere
398397 // in core. Reusing it here keeps the MVP aligned with the existing review
@@ -429,6 +428,44 @@ async fn run_guardian_subagent(
429428 parse_guardian_assessment ( last_agent_message. as_deref ( ) )
430429}
431430
431+ /// Builds the locked-down guardian config from the parent turn config.
432+ ///
433+ /// The guardian stays read-only and cannot request more permissions itself, but
434+ /// cloning the parent config preserves any already-configured managed network
435+ /// proxy / allowlist. When the parent session has edited that proxy state
436+ /// in-memory, we refresh from the live runtime config so the guardian sees the
437+ /// same current allowlist as the parent turn.
438+ fn build_guardian_subagent_config (
439+ parent_config : & Config ,
440+ live_network_config : Option < codex_network_proxy:: NetworkProxyConfig > ,
441+ ) -> anyhow:: Result < Config > {
442+ let mut guardian_config = parent_config. clone ( ) ;
443+ guardian_config. model = Some ( GUARDIAN_MODEL . to_string ( ) ) ;
444+ guardian_config. model_reasoning_effort =
445+ Some ( codex_protocol:: openai_models:: ReasoningEffort :: Low ) ;
446+ guardian_config. permissions . approval_policy = Constrained :: allow_only ( AskForApproval :: Never ) ;
447+ guardian_config. permissions . sandbox_policy =
448+ Constrained :: allow_only ( SandboxPolicy :: new_read_only_policy ( ) ) ;
449+ if let Some ( live_network_config) = live_network_config
450+ && guardian_config. permissions . network . is_some ( )
451+ {
452+ let network_constraints = guardian_config
453+ . config_layer_stack
454+ . requirements ( )
455+ . network
456+ . as_ref ( )
457+ . map ( |network| network. value . clone ( ) ) ;
458+ guardian_config. permissions . network = Some ( NetworkProxySpec :: from_config_and_constraints (
459+ live_network_config,
460+ network_constraints,
461+ ) ?) ;
462+ }
463+ let _ = guardian_config. features . disable ( Feature :: Collab ) ;
464+ let _ = guardian_config. features . disable ( Feature :: WebSearchRequest ) ;
465+ let _ = guardian_config. features . disable ( Feature :: WebSearchCached ) ;
466+ Ok ( guardian_config)
467+ }
468+
432469/// The model is asked for strict JSON, but we still accept a surrounding prose
433470/// wrapper so transient formatting drift fails less noisily during dogfooding.
434471fn parse_guardian_assessment ( text : Option < & str > ) -> anyhow:: Result < GuardianAssessment > {
@@ -526,7 +563,12 @@ impl GuardianRiskLevel {
526563#[ cfg( test) ]
527564mod tests {
528565 use super :: * ;
566+ use crate :: config:: NetworkProxySpec ;
567+ use crate :: config:: test_config;
568+ use crate :: config_loader:: NetworkConstraints ;
569+ use codex_network_proxy:: NetworkProxyConfig ;
529570 use codex_protocol:: models:: ContentItem ;
571+ use pretty_assertions:: assert_eq;
530572
531573 #[ test]
532574 fn build_guardian_transcript_keeps_original_numbering ( ) {
@@ -600,4 +642,60 @@ mod tests {
600642 assert_eq ! ( parsed. risk_score, 42 ) ;
601643 assert_eq ! ( parsed. risk_level, GuardianRiskLevel :: Medium ) ;
602644 }
645+
646+ #[ test]
647+ fn guardian_subagent_config_preserves_parent_network_proxy ( ) {
648+ let mut parent_config = test_config ( ) ;
649+ let network = NetworkProxySpec :: from_config_and_constraints (
650+ NetworkProxyConfig :: default ( ) ,
651+ Some ( NetworkConstraints {
652+ enabled : Some ( true ) ,
653+ allowed_domains : Some ( vec ! [ "github.com" . to_string( ) ] ) ,
654+ ..Default :: default ( )
655+ } ) ,
656+ )
657+ . expect ( "network proxy spec" ) ;
658+ parent_config. permissions . network = Some ( network. clone ( ) ) ;
659+
660+ let guardian_config =
661+ build_guardian_subagent_config ( & parent_config, None ) . expect ( "guardian config" ) ;
662+
663+ assert_eq ! ( guardian_config. permissions. network, Some ( network) ) ;
664+ assert_eq ! (
665+ guardian_config. permissions. approval_policy,
666+ Constrained :: allow_only( AskForApproval :: Never )
667+ ) ;
668+ assert_eq ! (
669+ guardian_config. permissions. sandbox_policy,
670+ Constrained :: allow_only( SandboxPolicy :: new_read_only_policy( ) )
671+ ) ;
672+ }
673+
674+ #[ test]
675+ fn guardian_subagent_config_uses_live_network_proxy_state ( ) {
676+ let mut parent_config = test_config ( ) ;
677+ let mut parent_network = NetworkProxyConfig :: default ( ) ;
678+ parent_network. network . enabled = true ;
679+ parent_network. network . allowed_domains = vec ! [ "parent.example" . to_string( ) ] ;
680+ parent_config. permissions . network = Some (
681+ NetworkProxySpec :: from_config_and_constraints ( parent_network, None )
682+ . expect ( "parent network proxy spec" ) ,
683+ ) ;
684+
685+ let mut live_network = NetworkProxyConfig :: default ( ) ;
686+ live_network. network . enabled = true ;
687+ live_network. network . allowed_domains = vec ! [ "github.com" . to_string( ) ] ;
688+
689+ let guardian_config =
690+ build_guardian_subagent_config ( & parent_config, Some ( live_network. clone ( ) ) )
691+ . expect ( "guardian config" ) ;
692+
693+ assert_eq ! (
694+ guardian_config. permissions. network,
695+ Some (
696+ NetworkProxySpec :: from_config_and_constraints( live_network, None )
697+ . expect( "live network proxy spec" )
698+ )
699+ ) ;
700+ }
603701}
0 commit comments