mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Add bounded environment transport for MXC launch requests (#44626)
## Why Sandbox policies can exceed Windows command-line limits. Provide an environment transport for these larger MXC launch requests. ## What changed - Make `MxcCommand` serializable and add JSON encoding and decoding through `CODEX_MXC_LAUNCH_*` environment variables. - Split payloads into UTF-8-safe chunks of at most 4,096 bytes, with a 1,000,000-byte payload limit and at most 256 chunks. - Reject malformed or duplicate transport entries and remove all transport variables during decoding, including on failure. Preserve unrelated environment variables. ## Testing Add tests for large Unicode and escaped-argument round trips, stale variable removal, malformed transport rejection and cleanup, and oversized payload rejection without modifying the environment. GitOrigin-RevId: 0e5ae2535ef313985a95eb8a5fd4884974dfb1eb
This commit is contained in:
Generated
+2
@@ -4076,6 +4076,8 @@ dependencies = [
|
||||
"codex-windows-sandbox",
|
||||
"learning_mode_windows",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tracelogging",
|
||||
|
||||
@@ -20,6 +20,8 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-windows-sandbox = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
wxc_common = { workspace = true }
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ use codex_protocol::models::PermissionProfile;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Typed inputs for the native policy adapter.
|
||||
pub mod transport;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MxcCommand {
|
||||
pub permissions: PermissionProfile,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Bounded launcher-only environment transport for policies too large for
|
||||
//! Windows command lines. Every transport variable is removed before spawn.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
|
||||
use crate::MxcCommand;
|
||||
|
||||
const PREFIX: &str = "CODEX_MXC_LAUNCH_";
|
||||
const COUNT: &str = "CODEX_MXC_LAUNCH_COUNT";
|
||||
const LENGTH: &str = "CODEX_MXC_LAUNCH_BYTES";
|
||||
const CHUNK_BYTES: usize = 4096;
|
||||
const MAX_CHUNKS: usize = 256;
|
||||
const MAX_BYTES: usize = 1_000_000;
|
||||
|
||||
fn is_transport_key(key: &str) -> bool {
|
||||
key.as_bytes()
|
||||
.get(..PREFIX.len())
|
||||
.is_some_and(|start| start.eq_ignore_ascii_case(PREFIX.as_bytes()))
|
||||
}
|
||||
|
||||
pub fn encode(command: &MxcCommand, env: &mut HashMap<String, String>) -> Result<()> {
|
||||
let payload = serde_json::to_string(command)?;
|
||||
ensure!(
|
||||
payload.len() <= MAX_BYTES,
|
||||
"MXC launcher payload exceeds {MAX_BYTES} bytes"
|
||||
);
|
||||
let mut chunks = Vec::new();
|
||||
let mut start = 0;
|
||||
while start < payload.len() {
|
||||
let mut end = (start + CHUNK_BYTES).min(payload.len());
|
||||
while !payload.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
chunks.push(payload[start..end].to_owned());
|
||||
start = end;
|
||||
}
|
||||
ensure!(
|
||||
chunks.len() <= MAX_CHUNKS,
|
||||
"MXC launcher payload has too many chunks"
|
||||
);
|
||||
env.retain(|key, _| !is_transport_key(key));
|
||||
env.insert(COUNT.to_owned(), chunks.len().to_string());
|
||||
env.insert(LENGTH.to_owned(), payload.len().to_string());
|
||||
for (index, chunk) in chunks.into_iter().enumerate() {
|
||||
env.insert(format!("{PREFIX}{index}"), chunk);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(windows, test))]
|
||||
pub fn decode(env: &mut HashMap<String, String>) -> Result<MxcCommand> {
|
||||
let mut encoded = HashMap::new();
|
||||
let mut duplicate = false;
|
||||
let mut oversized = false;
|
||||
env.retain(|key, value| {
|
||||
if !is_transport_key(key) {
|
||||
return true;
|
||||
}
|
||||
if encoded.len() >= MAX_CHUNKS + 2
|
||||
|| key.len() > PREFIX.len() + 32
|
||||
|| value.len() > CHUNK_BYTES
|
||||
{
|
||||
oversized = true;
|
||||
return false;
|
||||
}
|
||||
duplicate |= encoded
|
||||
.insert(key.to_ascii_uppercase(), std::mem::take(value))
|
||||
.is_some();
|
||||
false
|
||||
});
|
||||
ensure!(!oversized, "oversized MXC launcher environment");
|
||||
ensure!(!duplicate, "duplicate MXC launcher environment variables");
|
||||
let count: usize = encoded
|
||||
.get(COUNT)
|
||||
.context("missing MXC launcher chunk count")?
|
||||
.parse()?;
|
||||
let length: usize = encoded
|
||||
.get(LENGTH)
|
||||
.context("missing MXC launcher payload length")?
|
||||
.parse()?;
|
||||
ensure!(
|
||||
(1..=MAX_CHUNKS).contains(&count) && length <= MAX_BYTES,
|
||||
"invalid MXC launcher payload size"
|
||||
);
|
||||
ensure!(
|
||||
encoded.len() == count + 2,
|
||||
"unexpected MXC launcher environment variables"
|
||||
);
|
||||
let mut payload = String::with_capacity(length);
|
||||
for index in 0..count {
|
||||
let chunk = encoded
|
||||
.get(&format!("{PREFIX}{index}"))
|
||||
.context("missing MXC launcher chunk")?;
|
||||
ensure!(
|
||||
!chunk.is_empty() && chunk.len() <= CHUNK_BYTES,
|
||||
"invalid MXC launcher chunk size"
|
||||
);
|
||||
ensure!(
|
||||
payload.len() + chunk.len() <= length,
|
||||
"MXC launcher payload length mismatch"
|
||||
);
|
||||
payload.push_str(chunk);
|
||||
}
|
||||
ensure!(
|
||||
payload.len() == length,
|
||||
"MXC launcher payload length mismatch"
|
||||
);
|
||||
serde_json::from_str(&payload).context("invalid MXC launcher request")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "transport_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,81 @@
|
||||
//! Verify bounded transport, Unicode roundtrips, and child-environment cleanup.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::MxcCommand;
|
||||
|
||||
use super::CHUNK_BYTES;
|
||||
use super::COUNT;
|
||||
use super::LENGTH;
|
||||
use super::MAX_BYTES;
|
||||
use super::PREFIX;
|
||||
use super::decode;
|
||||
use super::encode;
|
||||
|
||||
fn command(args: Vec<String>) -> MxcCommand {
|
||||
MxcCommand {
|
||||
permissions: PermissionProfile::read_only(),
|
||||
sandbox_policy_cwd: PathBuf::from("workspace"),
|
||||
command: args,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_unicode_launch_roundtrips_and_never_reaches_child_environment() -> Result<()> {
|
||||
let args = vec![
|
||||
"codex-windows-mxc".to_owned(),
|
||||
"🧊".repeat(20_000),
|
||||
"a\"\\b".to_owned(),
|
||||
];
|
||||
let expected_env = HashMap::from([("CUSTOM".to_owned(), "value".to_owned())]);
|
||||
let mut env = expected_env.clone();
|
||||
env.insert("codex_mxc_launch_999".to_owned(), "spoofed".to_owned());
|
||||
let request = command(args);
|
||||
encode(&request, &mut env)?;
|
||||
assert!(!env.contains_key("codex_mxc_launch_999"));
|
||||
assert!(
|
||||
env.values()
|
||||
.all(|value| value.encode_utf16().count() < 8192)
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(decode(&mut env)?)?,
|
||||
serde_json::to_value(request)?
|
||||
);
|
||||
assert_eq!(env, expected_env);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_transport_is_rejected_and_removed() -> Result<()> {
|
||||
let expected_env = HashMap::from([("CUSTOM".to_owned(), "value".to_owned())]);
|
||||
let mut valid = expected_env.clone();
|
||||
encode(&command(vec!["program.exe".to_owned()]), &mut valid)?;
|
||||
for (key, value) in [
|
||||
(COUNT.to_owned(), "257".to_owned()),
|
||||
(LENGTH.to_owned(), "0".to_owned()),
|
||||
(format!("{PREFIX}0"), "x".repeat(CHUNK_BYTES + 1)),
|
||||
(COUNT.to_ascii_lowercase(), "1".to_owned()),
|
||||
] {
|
||||
let mut env = valid.clone();
|
||||
env.insert(key, value);
|
||||
assert!(decode(&mut env).is_err());
|
||||
assert_eq!(env, expected_env);
|
||||
}
|
||||
valid.remove(&format!("{PREFIX}0"));
|
||||
assert!(decode(&mut valid).is_err());
|
||||
assert_eq!(valid, expected_env);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_payload_fails_before_modifying_launcher_environment() {
|
||||
let expected = HashMap::from([("CUSTOM".to_owned(), "value".to_owned())]);
|
||||
let mut env = expected.clone();
|
||||
assert!(encode(&command(vec!["x".repeat(MAX_BYTES)]), &mut env).is_err());
|
||||
assert_eq!(env, expected);
|
||||
}
|
||||
Reference in New Issue
Block a user