feat(core): add structured network approval plumbing and policy decision model (#11672)

### Description
#### Summary
Introduces the core plumbing required for structured network approvals

#### What changed
- Added structured network policy decision modeling in core.
- Added approval payload/context types needed for network approval
semantics.
- Wired shell/unified-exec runtime plumbing to consume structured
decisions.
- Updated related core error/event surfaces for structured handling.
- Updated protocol plumbing used by core approval flow.
- Included small CLI debug sandbox compatibility updates needed by this
layer.

#### Why
establishes the minimal backend foundation for network approvals without
yet changing high-level orchestration or TUI behavior.

#### Notes
- Behavior remains constrained by existing requirements/config gating.
- Follow-up PRs in the stack handle orchestration, UX, and app-server
integration.

---------

Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
viyatb-oai
2026-02-13 20:18:12 -08:00
committed by GitHub
parent 854e91e422
commit b527ee2890
47 changed files with 1874 additions and 176 deletions

View File

@@ -15,6 +15,7 @@ use tokio::io::AsyncReadExt;
use tokio::io::BufReader;
use tokio::process::Child;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::error::CodexErr;
use crate::error::Result;
@@ -66,6 +67,7 @@ pub struct ExecParams {
pub expiration: ExecExpiration,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub network_attempt_id: Option<Uuid>,
pub sandbox_permissions: SandboxPermissions,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
pub justification: Option<String>,
@@ -184,13 +186,15 @@ pub async fn process_exec_tool_call(
mut env,
expiration,
network,
network_attempt_id,
sandbox_permissions,
windows_sandbox_level,
justification,
arg0: _,
} = params;
let network_attempt_id = network_attempt_id.map(|attempt_id| attempt_id.to_string());
if let Some(network) = network.as_ref() {
network.apply_to_env(&mut env);
network.apply_to_env_for_attempt(&mut env, network_attempt_id.as_deref());
}
let (program, args) = command.split_first().ok_or_else(|| {
CodexErr::Io(io::Error::new(
@@ -238,6 +242,7 @@ pub(crate) async fn execute_exec_env(
cwd,
env,
network,
network_attempt_id,
expiration,
sandbox,
windows_sandbox_level,
@@ -246,12 +251,18 @@ pub(crate) async fn execute_exec_env(
arg0,
} = env;
let network_attempt_id = match network_attempt_id.as_deref() {
Some(attempt_id) => Uuid::parse_str(attempt_id).ok(),
None => network.as_ref().map(|_| Uuid::new_v4()),
};
let params = ExecParams {
command,
cwd,
expiration,
env,
network,
network: network.clone(),
network_attempt_id,
sandbox_permissions,
windows_sandbox_level,
justification,
@@ -345,12 +356,14 @@ async fn exec_windows_sandbox(
cwd,
mut env,
network,
network_attempt_id,
expiration,
windows_sandbox_level,
..
} = params;
let network_attempt_id = network_attempt_id.map(|attempt_id| attempt_id.to_string());
if let Some(network) = network.as_ref() {
network.apply_to_env(&mut env);
network.apply_to_env_for_attempt(&mut env, network_attempt_id.as_deref());
}
// TODO(iceweasel-oai): run_windows_sandbox_capture should support all
@@ -490,6 +503,7 @@ fn finalize_exec_result(
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
return Err(CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(exec_output),
network_policy_decision: None,
}));
}
@@ -701,13 +715,19 @@ async fn exec(
let ExecParams {
command,
cwd,
env,
mut env,
network,
network_attempt_id,
arg0,
expiration,
windows_sandbox_level: _,
..
} = params;
let network_attempt_id = network_attempt_id.map(|attempt_id| attempt_id.to_string());
if let Some(network) = network.as_ref() {
network.apply_to_env_for_attempt(&mut env, network_attempt_id.as_deref());
}
let (program, args) = command.split_first().ok_or_else(|| {
CodexErr::Io(io::Error::new(
@@ -722,7 +742,10 @@ async fn exec(
arg0: arg0_ref,
cwd,
sandbox_policy,
network: network.as_ref(),
// The environment already has attempt-scoped proxy settings from
// apply_to_env_for_attempt above. Passing network here would reapply
// non-attempt proxy vars and drop attempt correlation metadata.
network: None,
stdio_policy: StdioPolicy::RedirectForShellTool,
env,
})
@@ -951,6 +974,17 @@ mod tests {
assert!(!is_likely_sandbox_denied(SandboxType::None, &output));
}
#[test]
fn sandbox_detection_ignores_network_policy_text_in_non_sandbox_mode() {
let output = make_exec_output(
0,
"",
"",
r#"CODEX_NETWORK_POLICY_DECISION {"decision":"ask","reason":"not_allowed","source":"decider","protocol":"http","host":"google.com","port":80}"#,
);
assert!(!is_likely_sandbox_denied(SandboxType::None, &output));
}
#[test]
fn sandbox_detection_uses_aggregated_output() {
let output = make_exec_output(
@@ -965,6 +999,21 @@ mod tests {
));
}
#[test]
fn sandbox_detection_ignores_network_policy_text_with_zero_exit_code() {
let output = make_exec_output(
0,
"",
"",
r#"CODEX_NETWORK_POLICY_DECISION {"decision":"ask","source":"decider","protocol":"http","host":"google.com","port":80}"#,
);
assert!(!is_likely_sandbox_denied(
SandboxType::LinuxSeccomp,
&output
));
}
#[tokio::test]
async fn read_capped_limits_retained_bytes() {
let (mut writer, reader) = tokio::io::duplex(1024);
@@ -1088,6 +1137,7 @@ mod tests {
expiration: 500.into(),
env,
network: None,
network_attempt_id: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,
@@ -1141,6 +1191,7 @@ mod tests {
expiration: ExecExpiration::Cancellation(cancel_token),
env,
network: None,
network_attempt_id: None,
sandbox_permissions: SandboxPermissions::UseDefault,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
justification: None,