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

@@ -18,6 +18,8 @@ use std::sync::Arc;
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;
@@ -71,12 +73,14 @@ impl PolicyParser {
#[derive(Debug, ProvidesStaticType)]
struct PolicyBuilder {
rules_by_program: MultiMap<String, RuleRef>,
network_rules: Vec<NetworkRule>,
}
impl PolicyBuilder {
fn new() -> Self {
Self {
rules_by_program: MultiMap::new(),
network_rules: Vec::new(),
}
}
@@ -85,8 +89,12 @@ impl PolicyBuilder {
.insert(rule.program().to_string(), rule);
}
fn add_network_rule(&mut self, rule: NetworkRule) {
self.network_rules.push(rule);
}
fn build(self) -> crate::policy::Policy {
crate::policy::Policy::new(self.rules_by_program)
crate::policy::Policy::from_parts(self.rules_by_program, self.network_rules)
}
}
@@ -142,6 +150,13 @@ fn parse_examples<'v>(examples: UnpackList<Value<'v>>) -> Result<Vec<Vec<String>
examples.items.into_iter().map(parse_example).collect()
}
fn parse_network_rule_decision(raw: &str) -> Result<Decision> {
match raw {
"deny" => Ok(Decision::Forbidden),
other => Decision::parse(other),
}
}
fn parse_example<'v>(value: Value<'v>) -> Result<Vec<String>> {
if let Some(raw) = value.unpack_str() {
parse_string_example(raw)
@@ -266,4 +281,31 @@ fn policy_builtins(builder: &mut GlobalsBuilder) {
rules.into_iter().for_each(|rule| builder.add_rule(rule));
Ok(NoneType)
}
fn network_rule<'v>(
host: &'v str,
protocol: &'v str,
decision: &'v str,
justification: Option<&'v str>,
eval: &mut Evaluator<'v, '_, '_>,
) -> anyhow::Result<NoneType> {
let protocol = NetworkRuleProtocol::parse(protocol)?;
let decision = parse_network_rule_decision(decision)?;
let justification = match justification {
Some(raw) if raw.trim().is_empty() => {
return Err(Error::InvalidRule("justification cannot be empty".to_string()).into());
}
Some(raw) => Some(raw.to_string()),
None => None,
};
let mut builder = policy_builder(eval);
builder.add_network_rule(NetworkRule {
host: crate::rule::normalize_network_rule_host(host)?,
protocol,
decision,
justification,
});
Ok(NoneType)
}
}