mirror of
https://github.com/openai/codex.git
synced 2026-04-26 07:35:29 +00:00
feat(network-proxy): add structured policy decision to blocked errors (#10420)
## Summary
Add explicit, model-visible network policy decision metadata to blocked
proxy responses/errors.
Introduces a standardized prefix line: `CODEX_NETWORK_POLICY_DECISION
{json}`
and wires it through blocked paths for:
- HTTP requests
- HTTPS CONNECT
- SOCKS5 TCP/UDP denials
## Why
The model should see *why* a request was blocked
(reason/source/protocol/host/port) so it can choose the correct next
action.
## Notes
- This PR is intentionally independent of config-layering/network-rule
runtime integration.
- Focus is blocked decision surface only.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
use crate::network_policy::NetworkDecisionSource;
|
||||
use crate::network_policy::NetworkPolicyDecision;
|
||||
use crate::network_policy::NetworkProtocol;
|
||||
use crate::reasons::REASON_DENIED;
|
||||
use crate::reasons::REASON_METHOD_NOT_ALLOWED;
|
||||
use crate::reasons::REASON_NOT_ALLOWED;
|
||||
@@ -8,6 +11,28 @@ use rama_http::StatusCode;
|
||||
use serde::Serialize;
|
||||
use tracing::error;
|
||||
|
||||
const NETWORK_POLICY_DECISION_PREFIX: &str = "CODEX_NETWORK_POLICY_DECISION";
|
||||
|
||||
pub struct PolicyDecisionDetails<'a> {
|
||||
pub decision: NetworkPolicyDecision,
|
||||
pub reason: &'a str,
|
||||
pub source: NetworkDecisionSource,
|
||||
pub protocol: NetworkProtocol,
|
||||
pub host: &'a str,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PolicyDecisionPayload<'a> {
|
||||
decision: &'a str,
|
||||
reason: &'a str,
|
||||
source: &'a str,
|
||||
protocol: &'a str,
|
||||
host: &'a str,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
pub fn text_response(status: StatusCode, body: &str) -> Response {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
@@ -57,11 +82,85 @@ pub fn blocked_message(reason: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blocked_text_response(reason: &str) -> Response {
|
||||
pub fn policy_decision_prefix(details: &PolicyDecisionDetails<'_>) -> String {
|
||||
let payload = PolicyDecisionPayload {
|
||||
decision: details.decision.as_str(),
|
||||
reason: details.reason,
|
||||
source: details.source.as_str(),
|
||||
protocol: details.protocol.as_policy_protocol(),
|
||||
host: details.host,
|
||||
port: details.port,
|
||||
};
|
||||
let payload_json = match serde_json::to_string(&payload) {
|
||||
Ok(json) => json,
|
||||
Err(err) => {
|
||||
error!("failed to serialize policy decision payload: {err}");
|
||||
"{}".to_string()
|
||||
}
|
||||
};
|
||||
format!("{NETWORK_POLICY_DECISION_PREFIX} {payload_json}")
|
||||
}
|
||||
|
||||
pub fn blocked_message_with_policy(reason: &str, details: &PolicyDecisionDetails<'_>) -> String {
|
||||
format!(
|
||||
"{}\n{}",
|
||||
policy_decision_prefix(details),
|
||||
blocked_message(reason)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn blocked_text_response_with_policy(
|
||||
reason: &str,
|
||||
details: &PolicyDecisionDetails<'_>,
|
||||
) -> Response {
|
||||
Response::builder()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.header("content-type", "text/plain")
|
||||
.header("x-proxy-error", blocked_header_value(reason))
|
||||
.body(Body::from(blocked_message(reason)))
|
||||
.body(Body::from(blocked_message_with_policy(reason, details)))
|
||||
.unwrap_or_else(|_| Response::new(Body::from("blocked")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::reasons::REASON_NOT_ALLOWED;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn policy_decision_prefix_serializes_expected_payload() {
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Ask,
|
||||
reason: REASON_NOT_ALLOWED,
|
||||
source: NetworkDecisionSource::Decider,
|
||||
protocol: NetworkProtocol::HttpsConnect,
|
||||
host: "api.example.com",
|
||||
port: 443,
|
||||
};
|
||||
|
||||
let line = policy_decision_prefix(&details);
|
||||
assert_eq!(
|
||||
line,
|
||||
r#"CODEX_NETWORK_POLICY_DECISION {"decision":"ask","reason":"not_allowed","source":"decider","protocol":"https_connect","host":"api.example.com","port":443}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_message_with_policy_includes_prefix_and_human_message() {
|
||||
let details = PolicyDecisionDetails {
|
||||
decision: NetworkPolicyDecision::Deny,
|
||||
reason: REASON_NOT_ALLOWED,
|
||||
source: NetworkDecisionSource::BaselinePolicy,
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "api.example.com",
|
||||
port: 80,
|
||||
};
|
||||
|
||||
let message = blocked_message_with_policy(REASON_NOT_ALLOWED, &details);
|
||||
assert_eq!(
|
||||
message,
|
||||
r#"CODEX_NETWORK_POLICY_DECISION {"decision":"deny","reason":"not_allowed","source":"baseline_policy","protocol":"http","host":"api.example.com","port":80}
|
||||
Codex blocked this request: domain not in allowlist."#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user