Skip to content

Commit 282a21a

Browse files
charley-oaicodex
andcommitted
Let guardian inherit live network allowlist
Refresh the guardian subagent's managed network proxy config from the parent session before launch so it sees the same current allowlist, including live in-session updates. Co-authored-by: Codex <noreply@openai.com>
1 parent b169530 commit 282a21a

2 files changed

Lines changed: 112 additions & 10 deletions

File tree

codex-rs/core/src/guardian.rs

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
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;
3032
use crate::compact::content_items_to_text;
3133
use crate::config::Config;
3234
use crate::config::Constrained;
35+
use crate::config::NetworkProxySpec;
3336
use crate::event_mapping::is_contextual_user_message_content;
3437
use crate::features::Feature;
3538
use 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.
434471
fn parse_guardian_assessment(text: Option<&str>) -> anyhow::Result<GuardianAssessment> {
@@ -526,7 +563,12 @@ impl GuardianRiskLevel {
526563
#[cfg(test)]
527564
mod 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
}

codex-rs/network-proxy/src/proxy.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ impl NetworkProxy {
390390
self.socks_addr
391391
}
392392

393+
pub async fn current_cfg(&self) -> Result<config::NetworkProxyConfig> {
394+
self.state.current_cfg().await
395+
}
396+
393397
pub async fn add_allowed_domain(&self, host: &str) -> Result<()> {
394398
self.state.add_allowed_domain(host).await
395399
}

0 commit comments

Comments
 (0)