feat(core): persist network approvals in execpolicy (#12357)

## Summary
Persist network approval allow/deny decisions as `network_rule(...)`
entries in execpolicy (not proxy config)

It adds `network_rule` parsing + append support in `codex-execpolicy`,
including `decision="prompt"` (parse-only; not compiled into proxy
allow/deny lists)
- compile execpolicy network rules into proxy allow/deny lists and
update the live proxy state on approval
- preserve requirements execpolicy `network_rule(...)` entries when
merging with file-based execpolicy
- reject broad wildcard hosts (for example `*`) for persisted
`network_rule(...)`
This commit is contained in:
viyatb-oai
2026-02-23 21:37:46 -08:00
committed by GitHub
Unverified
parent af215eb390
commit c3048ff90a
31 changed files with 1617 additions and 13 deletions
+102 -4
View File
@@ -10,8 +10,12 @@ use codex_network_proxy::NetworkProtocol;
use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::NetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::WarningEvent;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -19,6 +23,7 @@ use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::Notify;
use tokio::sync::RwLock;
use tracing::warn;
use uuid::Uuid;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -158,6 +163,7 @@ pub(crate) struct NetworkApprovalService {
call_outcomes: Mutex<HashMap<String, NetworkApprovalOutcome>>,
pending_host_approvals: Mutex<HashMap<HostApprovalKey, Arc<PendingHostApproval>>>,
session_approved_hosts: Mutex<HashSet<HostApprovalKey>>,
session_denied_hosts: Mutex<HashSet<HostApprovalKey>>,
}
impl Default for NetworkApprovalService {
@@ -167,6 +173,7 @@ impl Default for NetworkApprovalService {
call_outcomes: Mutex::new(HashMap::new()),
pending_host_approvals: Mutex::new(HashMap::new()),
session_approved_hosts: Mutex::new(HashSet::new()),
session_denied_hosts: Mutex::new(HashSet::new()),
}
}
}
@@ -272,6 +279,13 @@ impl NetworkApprovalService {
};
let key = HostApprovalKey::from_request(&request, protocol);
{
let denied_hosts = self.session_denied_hosts.lock().await;
if denied_hosts.contains(&key) {
return NetworkDecision::deny(REASON_NOT_ALLOWED);
}
}
{
let approved_hosts = self.session_approved_hosts.lock().await;
if approved_hosts.contains(&key) {
@@ -312,6 +326,10 @@ impl NetworkApprovalService {
let approval_id = Self::approval_id_for_key(&key);
let prompt_command = vec!["network-access".to_string(), target.clone()];
let network_approval_context = NetworkApprovalContext {
host: request.host.clone(),
protocol,
};
let approval_decision = session
.request_command_approval(
@@ -321,19 +339,86 @@ impl NetworkApprovalService {
prompt_command,
turn_context.cwd.clone(),
Some(prompt_reason),
Some(NetworkApprovalContext {
host: request.host.clone(),
protocol,
}),
Some(network_approval_context.clone()),
None,
)
.await;
let mut cache_session_deny = false;
let resolved = match approval_decision {
ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
PendingApprovalDecision::AllowOnce
}
ReviewDecision::ApprovedForSession => PendingApprovalDecision::AllowForSession,
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => {
match session
.persist_network_policy_amendment(
&network_policy_amendment,
&network_approval_context,
)
.await
{
Ok(()) => {
session
.record_network_policy_amendment_message(
&turn_context.sub_id,
&network_policy_amendment,
)
.await;
}
Err(err) => {
let message =
format!("Failed to apply network policy amendment: {err}");
warn!("{message}");
session
.send_event_raw(Event {
id: turn_context.sub_id.clone(),
msg: EventMsg::Warning(WarningEvent { message }),
})
.await;
}
}
PendingApprovalDecision::AllowForSession
}
NetworkPolicyRuleAction::Deny => {
match session
.persist_network_policy_amendment(
&network_policy_amendment,
&network_approval_context,
)
.await
{
Ok(()) => {
session
.record_network_policy_amendment_message(
&turn_context.sub_id,
&network_policy_amendment,
)
.await;
}
Err(err) => {
let message =
format!("Failed to apply network policy amendment: {err}");
warn!("{message}");
session
.send_event_raw(Event {
id: turn_context.sub_id.clone(),
msg: EventMsg::Warning(WarningEvent { message }),
})
.await;
}
}
self.record_outcome_for_single_active_call(
NetworkApprovalOutcome::DeniedByUser,
)
.await;
cache_session_deny = true;
PendingApprovalDecision::Deny
}
},
ReviewDecision::Denied | ReviewDecision::Abort => {
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByUser)
.await;
@@ -342,10 +427,23 @@ impl NetworkApprovalService {
};
if matches!(resolved, PendingApprovalDecision::AllowForSession) {
{
let mut denied_hosts = self.session_denied_hosts.lock().await;
denied_hosts.remove(&key);
}
let mut approved_hosts = self.session_approved_hosts.lock().await;
approved_hosts.insert(key.clone());
}
if cache_session_deny {
{
let mut approved_hosts = self.session_approved_hosts.lock().await;
approved_hosts.remove(&key);
}
let mut denied_hosts = self.session_denied_hosts.lock().await;
denied_hosts.insert(key.clone());
}
pending.set_decision(resolved).await;
let mut pending_approvals = self.pending_host_approvals.lock().await;
pending_approvals.remove(&key);
+17
View File
@@ -27,6 +27,7 @@ use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::default_exec_approval_requirement;
use codex_otel::ToolDecisionSource;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::NetworkPolicyRuleAction;
use codex_protocol::protocol::ReviewDecision;
pub(crate) struct ToolOrchestrator {
@@ -145,6 +146,14 @@ impl ToolOrchestrator {
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::ApprovedForSession => {}
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => {}
NetworkPolicyRuleAction::Deny => {
return Err(ToolError::Rejected("rejected by user".to_string()));
}
},
}
already_approved = true;
}
@@ -273,6 +282,14 @@ impl ToolOrchestrator {
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::ApprovedForSession => {}
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => {}
NetworkPolicyRuleAction::Deny => {
return Err(ToolError::Rejected("rejected by user".to_string()));
}
},
}
}