feat(network-proxy): add embedded OTEL policy audit logging (#12046)

**PR Summary**

This PR adds embedded-only OTEL policy audit logging for
`codex-network-proxy` and threads audit metadata from `codex-core` into
managed proxy startup.

### What changed
- Added structured audit event emission in `network_policy.rs` with
target `codex_otel.network_proxy`.
- Emitted:
- `codex.network_proxy.domain_policy_decision` once per domain-policy
evaluation.
  - `codex.network_proxy.block_decision` for non-domain denies.
- Added required policy/network fields, RFC3339 UTC millisecond
`event.timestamp`, and fallback defaults (`http.request.method="none"`,
`client.address="unknown"`).
- Added non-domain deny audit emission in HTTP/SOCKS handlers for
mode-guard and proxy-state denies, including unix-socket deny paths.
- Added `REASON_UNIX_SOCKET_UNSUPPORTED` and used it for unsupported
unix-socket auditing.
- Added `NetworkProxyAuditMetadata` to runtime/state, re-exported from
`lib.rs` and `state.rs`.
- Added `start_proxy_with_audit_metadata(...)` in core config, with
`start_proxy()` delegating to default metadata.
- Wired metadata construction in `codex.rs` from session/auth context,
including originator sanitization for OTEL-safe tagging.
- Updated `network-proxy/README.md` with embedded-mode audit schema and
behavior notes.
- Refactored HTTP block-audit emission to a small local helper to reduce
duplication.
- Preserved existing unix-socket proxy-disabled host/path behavior for
responses and blocked history while using an audit-only endpoint
override (`server.address="unix-socket"`, `server.port=0`).

### Explicit exclusions
- No standalone proxy OTEL startup work.
- No `main.rs` binary wiring.
- No `standalone_otel.rs`.
- No standalone docs/tests.

### Tests
- Extended `network_policy.rs` tests for event mapping, metadata
propagation, fallbacks, timestamp format, and target prefix.
- Extended HTTP tests to assert unix-socket deny block audit events.
- Extended SOCKS tests to cover deny emission from handler deny
branches.
- Added/updated core tests to verify audit metadata threading into
managed proxy state.

### Validation run
- `just fmt`
- `cargo test -p codex-network-proxy` 
- `cargo test -p codex-core` ran with one unrelated flaky timeout
(`shell_snapshot::tests::snapshot_shell_does_not_inherit_stdin`), and
the test passed when rerun directly 

---------

Co-authored-by: viyatb-oai <viyatb@openai.com>
This commit is contained in:
mcgrew-oai
2026-02-25 11:46:37 -05:00
committed by GitHub
Unverified
parent 8362b79cb4
commit 9a393c9b6f
16 changed files with 1592 additions and 657 deletions
+26 -6
View File
@@ -58,6 +58,7 @@ use codex_hooks::HookResult;
use codex_hooks::Hooks;
use codex_hooks::HooksConfig;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_network_proxy::normalize_host;
use codex_protocol::ThreadId;
use codex_protocol::approvals::ExecPolicyAmendment;
@@ -877,6 +878,7 @@ impl Session {
network_policy_decider: Option<Arc<dyn codex_network_proxy::NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn codex_network_proxy::BlockedRequestObserver>>,
managed_network_requirements_enabled: bool,
audit_metadata: NetworkProxyAuditMetadata,
) -> anyhow::Result<(StartedNetworkProxy, SessionNetworkProxyRuntime)> {
let network_proxy = spec
.start_proxy(
@@ -884,6 +886,7 @@ impl Session {
network_policy_decider,
blocked_request_observer,
managed_network_requirements_enabled,
audit_metadata,
)
.await
.map_err(|err| anyhow::anyhow!("failed to start managed network proxy: {err}"))?;
@@ -1198,21 +1201,37 @@ impl Session {
let auth = auth.as_ref();
let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from);
let account_id = auth.and_then(CodexAuth::get_account_id);
let account_email = auth.and_then(CodexAuth::get_account_email);
let originator = crate::default_client::originator().value;
let terminal_type = terminal::user_agent();
let session_model = session_configuration.collaboration_mode.model().to_string();
let mut otel_manager = OtelManager::new(
conversation_id,
session_configuration.collaboration_mode.model(),
session_configuration.collaboration_mode.model(),
auth.and_then(CodexAuth::get_account_id),
auth.and_then(CodexAuth::get_account_email),
session_model.as_str(),
session_model.as_str(),
account_id.clone(),
account_email.clone(),
auth_mode,
crate::default_client::originator().value,
originator.clone(),
config.otel.log_user_prompt,
terminal::user_agent(),
terminal_type.clone(),
session_configuration.session_source.clone(),
);
if let Some(service_name) = session_configuration.metrics_service_name.as_deref() {
otel_manager = otel_manager.with_metrics_service_name(service_name);
}
let network_proxy_audit_metadata = NetworkProxyAuditMetadata {
conversation_id: Some(conversation_id.to_string()),
app_version: Some(env!("CARGO_PKG_VERSION").to_string()),
user_account_id: account_id,
auth_mode: auth_mode.map(|mode| mode.to_string()),
originator: Some(originator),
user_email: account_email,
terminal_type: Some(terminal_type),
model: Some(session_model.clone()),
slug: Some(session_model),
};
config.features.emit_metrics(&otel_manager);
otel_manager.counter(
"codex.thread.started",
@@ -1319,6 +1338,7 @@ impl Session {
network_policy_decider.as_ref().map(Arc::clone),
blocked_request_observer.as_ref().map(Arc::clone),
managed_network_requirements_enabled,
network_proxy_audit_metadata,
)
.await?;
(Some(network_proxy), Some(session_network_proxy))
+1
View File
@@ -100,6 +100,7 @@ pub mod types;
pub use codex_config::Constrained;
pub use codex_config::ConstraintError;
pub use codex_config::ConstraintResult;
pub use codex_network_proxy::NetworkProxyAuditMetadata;
pub use network_proxy_spec::NetworkProxySpec;
pub use network_proxy_spec::StartedNetworkProxy;
+44 -6
View File
@@ -6,6 +6,7 @@ use codex_network_proxy::ConfigState;
use codex_network_proxy::NetworkDecision;
use codex_network_proxy::NetworkPolicyDecider;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyConstraints;
use codex_network_proxy::NetworkProxyHandle;
@@ -106,13 +107,9 @@ impl NetworkProxySpec {
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
enable_network_approval_flow: bool,
audit_metadata: NetworkProxyAuditMetadata,
) -> std::io::Result<StartedNetworkProxy> {
let state =
build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| {
std::io::Error::other(format!("failed to build network proxy state: {err}"))
})?;
let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone()));
let state = NetworkProxyState::with_reloader(state, reloader);
let state = self.build_state_with_audit_metadata(audit_metadata)?;
let mut builder = NetworkProxy::builder().state(Arc::new(state));
if enable_network_approval_flow
&& matches!(
@@ -142,6 +139,22 @@ impl NetworkProxySpec {
Ok(StartedNetworkProxy::new(proxy, handle))
}
fn build_state_with_audit_metadata(
&self,
audit_metadata: NetworkProxyAuditMetadata,
) -> std::io::Result<NetworkProxyState> {
let state =
build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| {
std::io::Error::other(format!("failed to build network proxy state: {err}"))
})?;
let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone()));
Ok(NetworkProxyState::with_reloader_and_audit_metadata(
state,
reloader,
audit_metadata,
))
}
fn apply_requirements(
mut config: NetworkProxyConfig,
requirements: &NetworkConstraints,
@@ -205,3 +218,28 @@ impl NetworkProxySpec {
(config, constraints)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn build_state_with_audit_metadata_threads_metadata_to_state() {
let spec = NetworkProxySpec {
config: NetworkProxyConfig::default(),
constraints: NetworkProxyConstraints::default(),
};
let metadata = NetworkProxyAuditMetadata {
conversation_id: Some("conversation-1".to_string()),
app_version: Some("1.2.3".to_string()),
user_account_id: Some("acct-1".to_string()),
..NetworkProxyAuditMetadata::default()
};
let state = spec
.build_state_with_audit_metadata(metadata.clone())
.expect("state should build");
assert_eq!(state.audit_metadata(), &metadata);
}
}