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

View File

@@ -1,11 +1,14 @@
use crate::decision::Decision;
use crate::error::Error;
use crate::error::Result;
use crate::rule::NetworkRule;
use crate::rule::NetworkRuleProtocol;
use crate::rule::PatternToken;
use crate::rule::PrefixPattern;
use crate::rule::PrefixRule;
use crate::rule::RuleMatch;
use crate::rule::RuleRef;
use crate::rule::normalize_network_rule_host;
use multimap::MultiMap;
use serde::Deserialize;
use serde::Serialize;
@@ -16,11 +19,22 @@ type HeuristicsFallback<'a> = Option<&'a dyn Fn(&[String]) -> Decision>;
#[derive(Clone, Debug)]
pub struct Policy {
rules_by_program: MultiMap<String, RuleRef>,
network_rules: Vec<NetworkRule>,
}
impl Policy {
pub fn new(rules_by_program: MultiMap<String, RuleRef>) -> Self {
Self { rules_by_program }
Self::from_parts(rules_by_program, Vec::new())
}
pub fn from_parts(
rules_by_program: MultiMap<String, RuleRef>,
network_rules: Vec<NetworkRule>,
) -> Self {
Self {
rules_by_program,
network_rules,
}
}
pub fn empty() -> Self {
@@ -31,6 +45,10 @@ impl Policy {
&self.rules_by_program
}
pub fn network_rules(&self) -> &[NetworkRule] {
&self.network_rules
}
pub fn get_allowed_prefixes(&self) -> Vec<Vec<String>> {
let mut prefixes = Vec::new();
@@ -77,6 +95,51 @@ impl Policy {
Ok(())
}
pub fn add_network_rule(
&mut self,
host: &str,
protocol: NetworkRuleProtocol,
decision: Decision,
justification: Option<String>,
) -> Result<()> {
let host = normalize_network_rule_host(host)?;
if let Some(raw) = justification.as_deref()
&& raw.trim().is_empty()
{
return Err(Error::InvalidRule(
"justification cannot be empty".to_string(),
));
}
self.network_rules.push(NetworkRule {
host,
protocol,
decision,
justification,
});
Ok(())
}
pub fn compiled_network_domains(&self) -> (Vec<String>, Vec<String>) {
let mut allowed = Vec::new();
let mut denied = Vec::new();
for rule in &self.network_rules {
match rule.decision {
Decision::Allow => {
denied.retain(|entry| entry != &rule.host);
upsert_domain(&mut allowed, &rule.host);
}
Decision::Forbidden => {
allowed.retain(|entry| entry != &rule.host);
upsert_domain(&mut denied, &rule.host);
}
Decision::Prompt => {}
}
}
(allowed, denied)
}
pub fn check<F>(&self, cmd: &[String], heuristics_fallback: &F) -> Evaluation
where
F: Fn(&[String]) -> Decision,
@@ -140,6 +203,11 @@ impl Policy {
}
}
fn upsert_domain(entries: &mut Vec<String>, host: &str) {
entries.retain(|entry| entry != host);
entries.push(host.to_string());
}
fn render_pattern_token(token: &PatternToken) -> String {
match token {
PatternToken::Single(value) => value.clone(),