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
parent af215eb390
commit c3048ff90a
31 changed files with 1617 additions and 13 deletions
+150
View File
@@ -55,8 +55,11 @@ use codex_hooks::HookResult;
use codex_hooks::Hooks;
use codex_hooks::HooksConfig;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::normalize_host;
use codex_protocol::ThreadId;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::config_types::WebSearchMode;
@@ -165,6 +168,7 @@ use crate::mentions::build_connector_slug_counts;
use crate::mentions::build_skill_name_counts;
use crate::mentions::collect_explicit_app_ids;
use crate::mentions::collect_tool_mentions_from_messages;
use crate::network_policy_decision::execpolicy_network_rule_amendment;
use crate::project_doc::get_user_instructions;
use crate::proposed_plan_parser::ProposedPlanParser;
use crate::proposed_plan_parser::ProposedPlanSegment;
@@ -2377,6 +2381,103 @@ impl Session {
}
}
pub(crate) async fn persist_network_policy_amendment(
&self,
amendment: &NetworkPolicyAmendment,
network_approval_context: &NetworkApprovalContext,
) -> anyhow::Result<()> {
let host =
Self::validated_network_policy_amendment_host(amendment, network_approval_context)?;
let codex_home = self
.state
.lock()
.await
.session_configuration
.codex_home()
.clone();
let execpolicy_amendment =
execpolicy_network_rule_amendment(amendment, network_approval_context, &host);
if let Some(started_network_proxy) = self.services.network_proxy.as_ref() {
let proxy = started_network_proxy.proxy();
match amendment.action {
NetworkPolicyRuleAction::Allow => proxy
.add_allowed_domain(&host)
.await
.map_err(|err| anyhow::anyhow!("failed to update runtime allowlist: {err}"))?,
NetworkPolicyRuleAction::Deny => proxy
.add_denied_domain(&host)
.await
.map_err(|err| anyhow::anyhow!("failed to update runtime denylist: {err}"))?,
}
}
self.services
.exec_policy
.append_network_rule_and_update(
&codex_home,
&host,
execpolicy_amendment.protocol,
execpolicy_amendment.decision,
Some(execpolicy_amendment.justification),
)
.await
.map_err(|err| {
anyhow::anyhow!("failed to persist network policy amendment to execpolicy: {err}")
})?;
Ok(())
}
fn validated_network_policy_amendment_host(
amendment: &NetworkPolicyAmendment,
network_approval_context: &NetworkApprovalContext,
) -> anyhow::Result<String> {
let approved_host = normalize_host(&network_approval_context.host);
let amendment_host = normalize_host(&amendment.host);
if amendment_host != approved_host {
return Err(anyhow::anyhow!(
"network policy amendment host '{}' does not match approved host '{}'",
amendment.host,
network_approval_context.host
));
}
Ok(approved_host)
}
pub(crate) async fn record_network_policy_amendment_message(
&self,
sub_id: &str,
amendment: &NetworkPolicyAmendment,
) {
let (action, list_name) = match amendment.action {
NetworkPolicyRuleAction::Allow => ("Allowed", "allowlist"),
NetworkPolicyRuleAction::Deny => ("Denied", "denylist"),
};
let text = format!(
"{action} network rule saved in execpolicy ({list_name}): {}",
amendment.host
);
let message: ResponseItem = DeveloperInstructions::new(text.clone()).into();
if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await {
self.record_conversation_items(&turn_context, std::slice::from_ref(&message))
.await;
return;
}
if self
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
}])
.await
.is_err()
{
warn!("no active turn found to record network policy amendment message for {sub_id}");
}
}
/// Emit an exec approval request event and await the user's decision.
///
/// The request is keyed by `call_id` + `approval_id` so matching responses are delivered
@@ -2414,6 +2515,18 @@ impl Session {
}
let parsed_cmd = parse_command(&command);
let proposed_network_policy_amendments = network_approval_context.as_ref().map(|context| {
vec![
NetworkPolicyAmendment {
host: context.host.clone(),
action: NetworkPolicyRuleAction::Allow,
},
NetworkPolicyAmendment {
host: context.host.clone(),
action: NetworkPolicyRuleAction::Deny,
},
]
});
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id,
approval_id,
@@ -2423,6 +2536,7 @@ impl Session {
reason,
network_approval_context,
proposed_execpolicy_amendment,
proposed_network_policy_amendments,
parsed_cmd,
});
self.send_event(turn_context, event).await;
@@ -6120,6 +6234,7 @@ mod tests {
use crate::protocol::CompactedItem;
use crate::protocol::CreditsSnapshot;
use crate::protocol::InitialHistory;
use crate::protocol::NetworkApprovalProtocol;
use crate::protocol::RateLimitSnapshot;
use crate::protocol::RateLimitWindow;
use crate::protocol::ResumedHistory;
@@ -6246,6 +6361,41 @@ mod tests {
})
}
#[test]
fn validated_network_policy_amendment_host_allows_normalized_match() {
let amendment = NetworkPolicyAmendment {
host: "ExAmPlE.Com.:443".to_string(),
action: NetworkPolicyRuleAction::Allow,
};
let context = NetworkApprovalContext {
host: "example.com".to_string(),
protocol: NetworkApprovalProtocol::Https,
};
let host = Session::validated_network_policy_amendment_host(&amendment, &context)
.expect("normalized hosts should match");
assert_eq!(host, "example.com");
}
#[test]
fn validated_network_policy_amendment_host_rejects_mismatch() {
let amendment = NetworkPolicyAmendment {
host: "evil.example.com".to_string(),
action: NetworkPolicyRuleAction::Deny,
};
let context = NetworkApprovalContext {
host: "api.example.com".to_string(),
protocol: NetworkApprovalProtocol::Https,
};
let err = Session::validated_network_policy_amendment_host(&amendment, &context)
.expect_err("mismatched hosts should be rejected");
let message = err.to_string();
assert!(message.contains("does not match approved host"));
}
#[tokio::test]
async fn get_base_instructions_no_user_content() {
let prompt_with_apply_patch_instructions =
+78 -1
View File
@@ -13,10 +13,12 @@ use codex_execpolicy::AmendError;
use codex_execpolicy::Decision;
use codex_execpolicy::Error as ExecPolicyRuleError;
use codex_execpolicy::Evaluation;
use codex_execpolicy::NetworkRuleProtocol;
use codex_execpolicy::Policy;
use codex_execpolicy::PolicyParser;
use codex_execpolicy::RuleMatch;
use codex_execpolicy::blocking_append_allow_prefix_rule;
use codex_execpolicy::blocking_append_network_rule;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
@@ -293,6 +295,43 @@ impl ExecPolicyManager {
self.policy.store(Arc::new(updated_policy));
Ok(())
}
pub(crate) async fn append_network_rule_and_update(
&self,
codex_home: &Path,
host: &str,
protocol: NetworkRuleProtocol,
decision: Decision,
justification: Option<String>,
) -> Result<(), ExecPolicyUpdateError> {
let policy_path = default_policy_path(codex_home);
let host = host.to_string();
spawn_blocking({
let policy_path = policy_path.clone();
let host = host.clone();
let justification = justification.clone();
move || {
blocking_append_network_rule(
&policy_path,
&host,
protocol,
decision,
justification.as_deref(),
)
}
})
.await
.map_err(|source| ExecPolicyUpdateError::JoinBlockingTask { source })?
.map_err(|source| ExecPolicyUpdateError::AppendRule {
path: policy_path,
source,
})?;
let mut updated_policy = self.current().as_ref().clone();
updated_policy.add_network_rule(&host, protocol, decision, justification)?;
self.policy.store(Arc::new(updated_policy));
Ok(())
}
}
impl Default for ExecPolicyManager {
@@ -440,7 +479,10 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
}
}
Ok(Policy::new(combined_rules))
let mut combined_network_rules = policy.network_rules().to_vec();
combined_network_rules.extend(requirements_policy.as_ref().network_rules().iter().cloned());
Ok(Policy::from_parts(combined_rules, combined_network_rules))
}
/// If a command is not matched by any execpolicy rule, derive a [`Decision`].
@@ -914,6 +956,41 @@ mod tests {
);
}
#[tokio::test]
async fn merges_requirements_exec_policy_network_rules() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let mut requirements_exec_policy = Policy::empty();
requirements_exec_policy.add_network_rule(
"blocked.example.com",
codex_execpolicy::NetworkRuleProtocol::Https,
Decision::Forbidden,
None,
)?;
let requirements = ConfigRequirements {
exec_policy: Some(codex_config::Sourced::new(
codex_config::RequirementsExecPolicy::new(requirements_exec_policy),
codex_config::RequirementSource::Unknown,
)),
..ConfigRequirements::default()
};
let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path())?;
let layer = ConfigLayerEntry::new(
ConfigLayerSource::Project { dot_codex_folder },
TomlValue::Table(Default::default()),
);
let config_stack =
ConfigLayerStack::new(vec![layer], requirements, ConfigRequirementsToml::default())?;
let policy = load_exec_policy(&config_stack).await?;
let (allowed, denied) = policy.compiled_network_domains();
assert!(allowed.is_empty());
assert_eq!(denied, vec!["blocked.example.com".to_string()]);
Ok(())
}
#[tokio::test]
async fn ignores_policies_outside_policy_dir() {
let temp_dir = tempdir().expect("create temp dir");
@@ -1,8 +1,12 @@
use codex_execpolicy::Decision as ExecPolicyDecision;
use codex_execpolicy::NetworkRuleProtocol as ExecPolicyNetworkRuleProtocol;
use codex_network_proxy::BlockedRequest;
use codex_network_proxy::NetworkDecisionSource;
use codex_network_proxy::NetworkPolicyDecision;
use codex_protocol::approvals::NetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
@@ -17,6 +21,13 @@ pub struct NetworkPolicyDecisionPayload {
pub port: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExecPolicyNetworkRuleAmendment {
pub protocol: ExecPolicyNetworkRuleProtocol,
pub decision: ExecPolicyDecision,
pub justification: String,
}
impl NetworkPolicyDecisionPayload {
pub(crate) fn is_ask_from_decider(&self) -> bool {
self.decision == NetworkPolicyDecision::Ask && self.source == NetworkDecisionSource::Decider
@@ -79,10 +90,42 @@ pub(crate) fn denied_network_policy_message(blocked: &BlockedRequest) -> Option<
))
}
pub(crate) fn execpolicy_network_rule_amendment(
amendment: &NetworkPolicyAmendment,
network_approval_context: &NetworkApprovalContext,
host: &str,
) -> ExecPolicyNetworkRuleAmendment {
let protocol = match network_approval_context.protocol {
NetworkApprovalProtocol::Http => ExecPolicyNetworkRuleProtocol::Http,
NetworkApprovalProtocol::Https => ExecPolicyNetworkRuleProtocol::Https,
NetworkApprovalProtocol::Socks5Tcp => ExecPolicyNetworkRuleProtocol::Socks5Tcp,
NetworkApprovalProtocol::Socks5Udp => ExecPolicyNetworkRuleProtocol::Socks5Udp,
};
let (decision, action_verb) = match amendment.action {
NetworkPolicyRuleAction::Allow => (ExecPolicyDecision::Allow, "Allow"),
NetworkPolicyRuleAction::Deny => (ExecPolicyDecision::Forbidden, "Deny"),
};
let protocol_label = match network_approval_context.protocol {
NetworkApprovalProtocol::Http => "http",
NetworkApprovalProtocol::Https => "https_connect",
NetworkApprovalProtocol::Socks5Tcp => "socks5_tcp",
NetworkApprovalProtocol::Socks5Udp => "socks5_udp",
};
let justification = format!("{action_verb} {protocol_label} access to {host}");
ExecPolicyNetworkRuleAmendment {
protocol,
decision,
justification,
}
}
#[cfg(test)]
mod tests {
use super::*;
use codex_network_proxy::BlockedRequest;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use pretty_assertions::assert_eq;
#[test]
@@ -211,6 +254,27 @@ mod tests {
assert_eq!(payload.protocol, Some(NetworkApprovalProtocol::Https));
}
#[test]
fn execpolicy_network_rule_amendment_maps_protocol_action_and_justification() {
let amendment = NetworkPolicyAmendment {
action: NetworkPolicyRuleAction::Deny,
host: "example.com".to_string(),
};
let context = NetworkApprovalContext {
host: "example.com".to_string(),
protocol: NetworkApprovalProtocol::Socks5Udp,
};
assert_eq!(
execpolicy_network_rule_amendment(&amendment, &context, "example.com"),
ExecPolicyNetworkRuleAmendment {
protocol: ExecPolicyNetworkRuleProtocol::Socks5Udp,
decision: ExecPolicyDecision::Forbidden,
justification: "Deny socks5_udp access to example.com".to_string(),
}
);
}
#[test]
fn denied_network_policy_message_requires_deny_decision() {
let blocked = BlockedRequest {
+93 -2
View File
@@ -6,6 +6,9 @@ use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigLayerStackOrdering;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::load_config_layers_state;
use crate::exec_policy::ExecPolicyError;
use crate::exec_policy::format_exec_policy_error_with_source;
use crate::exec_policy::load_exec_policy;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
@@ -18,6 +21,7 @@ use codex_network_proxy::NetworkProxyConstraintError;
use codex_network_proxy::NetworkProxyConstraints;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::build_config_state;
use codex_network_proxy::normalize_host;
use codex_network_proxy::validate_policy_against_constraints;
use serde::Deserialize;
use std::path::PathBuf;
@@ -49,7 +53,21 @@ async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime
.await
.context("failed to load Codex config")?;
let config = config_from_layers(&config_layer_stack)?;
let (exec_policy, warning) = match load_exec_policy(&config_layer_stack).await {
Ok(policy) => (policy, None),
Err(err @ ExecPolicyError::ParsePolicy { .. }) => {
(codex_execpolicy::Policy::empty(), Some(err))
}
Err(err) => return Err(err.into()),
};
if let Some(err) = warning.as_ref() {
tracing::warn!(
"failed to parse execpolicy while building network proxy state: {}",
format_exec_policy_error_with_source(err)
);
}
let config = config_from_layers(&config_layer_stack, &exec_policy)?;
let constraints = enforce_trusted_constraints(&config_layer_stack, &config)?;
let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
@@ -175,15 +193,46 @@ fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesTo
}
}
fn config_from_layers(layers: &ConfigLayerStack) -> Result<NetworkProxyConfig> {
fn config_from_layers(
layers: &ConfigLayerStack,
exec_policy: &codex_execpolicy::Policy,
) -> Result<NetworkProxyConfig> {
let mut config = NetworkProxyConfig::default();
for layer in layers.get_layers(ConfigLayerStackOrdering::LowestPrecedenceFirst, false) {
let parsed = network_tables_from_toml(&layer.config)?;
apply_network_tables(&mut config, parsed);
}
apply_exec_policy_network_rules(&mut config, exec_policy);
Ok(config)
}
fn apply_exec_policy_network_rules(
config: &mut NetworkProxyConfig,
exec_policy: &codex_execpolicy::Policy,
) {
let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();
for host in allowed_domains {
upsert_network_domain(
&mut config.network.allowed_domains,
&mut config.network.denied_domains,
host,
);
}
for host in denied_domains {
upsert_network_domain(
&mut config.network.denied_domains,
&mut config.network.allowed_domains,
host,
);
}
}
fn upsert_network_domain(target: &mut Vec<String>, opposite: &mut Vec<String>, host: String) {
opposite.retain(|entry| normalize_host(entry) != host);
target.retain(|entry| normalize_host(entry) != host);
target.push(host);
}
fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
matches!(
layer,
@@ -260,6 +309,9 @@ impl ConfigReloader for MtimeConfigReloader {
mod tests {
use super::*;
use codex_execpolicy::Decision;
use codex_execpolicy::NetworkRuleProtocol;
use codex_execpolicy::Policy;
use pretty_assertions::assert_eq;
#[test]
@@ -292,6 +344,45 @@ allowed_domains = ["higher.example.com"]
assert_eq!(config.network.allowed_domains, vec!["higher.example.com"]);
}
#[test]
fn execpolicy_network_rules_overlay_network_lists() {
let mut config = NetworkProxyConfig::default();
config.network.allowed_domains = vec!["config.example.com".to_string()];
config.network.denied_domains = vec!["blocked.example.com".to_string()];
let mut exec_policy = Policy::empty();
exec_policy
.add_network_rule(
"blocked.example.com",
NetworkRuleProtocol::Https,
Decision::Allow,
None,
)
.expect("allow rule should be valid");
exec_policy
.add_network_rule(
"api.example.com",
NetworkRuleProtocol::Http,
Decision::Forbidden,
None,
)
.expect("deny rule should be valid");
apply_exec_policy_network_rules(&mut config, &exec_policy);
assert_eq!(
config.network.allowed_domains,
vec![
"config.example.com".to_string(),
"blocked.example.com".to_string()
]
);
assert_eq!(
config.network.denied_domains,
vec!["api.example.com".to_string()]
);
}
#[test]
fn apply_network_constraints_includes_allow_all_unix_sockets_flag() {
let config: toml::Value = toml::from_str(
+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()));
}
},
}
}
+12
View File
@@ -15,6 +15,8 @@ use crate::protocol::ExecCommandOutputDeltaEvent;
#[cfg(unix)]
use crate::protocol::ExecOutputStream;
#[cfg(unix)]
use crate::protocol::NetworkPolicyRuleAction;
#[cfg(unix)]
use crate::protocol::ReviewDecision;
#[cfg(unix)]
use anyhow::Context as _;
@@ -373,6 +375,16 @@ impl ZshExecBridge {
| ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
(WrapperExecAction::Run, None, false)
}
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => (WrapperExecAction::Run, None, false),
NetworkPolicyRuleAction::Deny => (
WrapperExecAction::Deny,
Some("command denied by host approval policy".to_string()),
true,
),
},
ReviewDecision::Denied => (
WrapperExecAction::Deny,
Some("command denied by host approval policy".to_string()),