use crate::session::turn_context::TurnContext;use crate::session::turn_context::TurnEnvironment;use crate::shell::Shell;use codex_protocol::models::ManagedFileSystemPermissions;use codex_protocol::models::PermissionProfile;use codex_protocol::permissions::FileSystemAccessMode;use codex_protocol::permissions::FileSystemPath;use codex_protocol::permissions::FileSystemSandboxEntry;use codex_protocol::permissions::FileSystemSpecialPath;use codex_protocol::protocol::TurnContextItem;use codex_protocol::protocol::TurnContextNetworkItem;use codex_utils_absolute_path::AbsolutePathBuf;use std::collections::HashSet;use std::path::PathBuf;use super::ContextualUserFragment;#[derive(Debug, Clone, PartialEq)]pub(crate) struct EnvironmentContext { pub(crate) environments: EnvironmentContextEnvironments, pub(crate) current_date: Option<String>, pub(crate) timezone: Option<String>, pub(crate) network: Option<NetworkContext>, pub(crate) filesystem: Option<FileSystemContext>, pub(crate) subagents: Option<String>,}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct EnvironmentContextEnvironment { pub(crate) id: String, pub(crate) cwd: AbsolutePathBuf, pub(crate) shell: String,}impl EnvironmentContextEnvironment { fn legacy(cwd: AbsolutePathBuf, shell: String) -> Self { Self { id: String::new(), cwd, shell, } } fn from_turn_environments(environments: &[TurnEnvironment], shell: &Shell) -> Vec<Self> { environments .iter() .filter_map(|environment| { // TODO(anp): Migrate EnvironmentContextEnvironment to PathUri so foreign // environments remain visible in model context. Some(Self { id: environment.environment_id.clone(), cwd: environment.cwd().to_abs_path().ok()?, shell: environment .shell .as_ref() .map(|shell| shell.name().to_string()) .unwrap_or_else(|| shell.name().to_string()), }) }) .collect() }}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) enum EnvironmentContextEnvironments { None, Single(EnvironmentContextEnvironment), Multiple(Vec<EnvironmentContextEnvironment>),}impl EnvironmentContextEnvironments { fn from_vec(environments: Vec<EnvironmentContextEnvironment>) -> Self { let mut environments = environments; match environments.pop() { None => Self::None, Some(environment) if environments.is_empty() => Self::Single(environment), Some(environment) => { environments.push(environment); Self::Multiple(environments) } } } fn equals_except_shell(&self, other: &Self) -> bool { match (self, other) { (Self::None, Self::None) => true, (Self::Single(left), Self::Single(right)) => left.cwd == right.cwd, (Self::Multiple(left), Self::Multiple(right)) => { left.len() == right.len() && left .iter() .zip(right.iter()) .all(|(left, right)| left.id == right.id && left.cwd == right.cwd) } _ => false, } }}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct FileSystemContext { workspace_roots: Vec<String>, permission_profile: FileSystemPermissionProfileContext,}#[derive(Debug, Clone, PartialEq, Eq)]enum FileSystemPermissionProfileContext { Managed(ManagedFileSystemContext), Disabled, External,}#[derive(Debug, Clone, PartialEq, Eq)]enum ManagedFileSystemContext { Restricted { entries: Vec<FileSystemSandboxEntry>, glob_scan_max_depth: Option<usize>, }, Unrestricted,}impl FileSystemContext { fn from_permission_profile( permission_profile: &PermissionProfile, workspace_roots: &[AbsolutePathBuf], ) -> Self { let permission_profile = permission_profile .clone() .materialize_project_roots_with_workspace_roots(workspace_roots); let workspace_roots = workspace_roots .iter() .map(|root| root.to_string_lossy().into_owned()) .collect(); let permission_profile = match permission_profile { PermissionProfile::Managed { file_system, .. } => { FileSystemPermissionProfileContext::Managed(ManagedFileSystemContext::from( file_system, )) } PermissionProfile::Disabled => FileSystemPermissionProfileContext::Disabled, PermissionProfile::External { .. } => FileSystemPermissionProfileContext::External, }; Self { workspace_roots, permission_profile, } } fn render(&self) -> String { let mut rendered = "<filesystem>".to_string(); if !self.workspace_roots.is_empty() { rendered.push_str("<workspace_roots>"); for root in &self.workspace_roots { push_text_element(&mut rendered, "root", root); } rendered.push_str("</workspace_roots>"); } self.permission_profile.render(&mut rendered); rendered.push_str("</filesystem>"); rendered }}impl From<ManagedFileSystemPermissions> for ManagedFileSystemContext { fn from(file_system: ManagedFileSystemPermissions) -> Self { match file_system { ManagedFileSystemPermissions::Restricted { mut entries, glob_scan_max_depth, } => { dedupe_file_system_entries(&mut entries); Self::Restricted { entries, glob_scan_max_depth: glob_scan_max_depth.map(usize::from), } } ManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, } }}impl FileSystemPermissionProfileContext { fn render(&self, rendered: &mut String) { match self { Self::Managed(file_system) => { rendered.push_str("<permission_profile type=\"managed\">"); file_system.render(rendered); rendered.push_str("</permission_profile>"); } Self::Disabled => { rendered.push_str( "<permission_profile type=\"disabled\"><file_system type=\"unrestricted\" /></permission_profile>", ); } Self::External => { rendered.push_str( "<permission_profile type=\"external\"><file_system type=\"external\" /></permission_profile>", ); } } }}impl ManagedFileSystemContext { fn render(&self, rendered: &mut String) { match self { Self::Restricted { entries, glob_scan_max_depth, } => { if entries.is_empty() && glob_scan_max_depth.is_none() { rendered.push_str("<file_system type=\"restricted\" />"); return; } rendered.push_str("<file_system type=\"restricted\""); if let Some(glob_scan_max_depth) = glob_scan_max_depth { rendered.push_str(&format!(" glob_scan_max_depth=\"{glob_scan_max_depth}\"")); } rendered.push('>'); for entry in entries { render_file_system_entry(rendered, entry); } rendered.push_str("</file_system>"); } Self::Unrestricted => { rendered.push_str("<file_system type=\"unrestricted\" />"); } } }}fn render_file_system_entry(rendered: &mut String, entry: &FileSystemSandboxEntry) { rendered.push_str("<entry access=\""); let access = entry.access.to_string(); rendered.push_str(&access); if entry.access == FileSystemAccessMode::Deny { rendered.push_str("\" escalatable=\"false"); } rendered.push_str("\">"); match &entry.path { FileSystemPath::Path { path } => { push_text_element(rendered, "path", path.to_string_lossy().as_ref()); } FileSystemPath::GlobPattern { pattern } => { push_text_element(rendered, "glob", pattern); } FileSystemPath::Special { value } => { let value = render_special_path(value); push_text_element(rendered, "special", &value); } } rendered.push_str("</entry>");}fn render_special_path(value: &FileSystemSpecialPath) -> String { match value { FileSystemSpecialPath::Root => ":root".to_string(), FileSystemSpecialPath::Minimal => ":minimal".to_string(), FileSystemSpecialPath::ProjectRoots { subpath } => { render_special_path_with_subpath(":workspace_roots", subpath) } FileSystemSpecialPath::Tmpdir => ":tmpdir".to_string(), FileSystemSpecialPath::SlashTmp => ":slash_tmp".to_string(), FileSystemSpecialPath::Unknown { path, subpath } => { render_special_path_with_subpath(path, subpath) } }}fn render_special_path_with_subpath(base: &str, subpath: &Option<PathBuf>) -> String { match subpath { Some(subpath) => format!("{base}/{}", subpath.display()), None => base.to_string(), }}fn dedupe_file_system_entries(entries: &mut Vec<FileSystemSandboxEntry>) { let mut seen = HashSet::new(); entries.retain(|entry| seen.insert(entry.clone()));}fn push_text_element(rendered: &mut String, name: &str, value: &str) { rendered.push_str(&format!("<{name}>")); push_xml_escaped_text(rendered, value); rendered.push_str(&format!("</{name}>"));}fn push_xml_escaped_text(rendered: &mut String, value: &str) { for ch in value.chars() { match ch { '&' => rendered.push_str("&"), '<' => rendered.push_str("<"), '>' => rendered.push_str(">"), '"' => rendered.push_str("""), '\'' => rendered.push_str("'"), _ => rendered.push(ch), } }}#[derive(Debug, Clone, PartialEq, Eq, Default)]pub(crate) struct NetworkContext { allowed_domains: Vec<String>, denied_domains: Vec<String>,}impl NetworkContext { pub(crate) fn new(allowed_domains: Vec<String>, denied_domains: Vec<String>) -> Self { Self { allowed_domains, denied_domains, } } fn render(&self) -> String { let mut rendered = "<network enabled=\"true\">".to_string(); Self::push_rendered_domain_element(&mut rendered, "allowed", &self.allowed_domains); Self::push_rendered_domain_element(&mut rendered, "denied", &self.denied_domains); rendered.push_str("</network>"); rendered } fn push_rendered_domain_element(rendered_network: &mut String, name: &str, domains: &[String]) { if domains.is_empty() { return; } rendered_network.push_str(&format!("<{name}>")); rendered_network.push_str(&domains.join(",")); rendered_network.push_str(&format!("</{name}>")); }}impl EnvironmentContext { pub(crate) fn new( environments: Vec<EnvironmentContextEnvironment>, current_date: Option<String>, timezone: Option<String>, network: Option<NetworkContext>, subagents: Option<String>, ) -> Self { Self { environments: EnvironmentContextEnvironments::from_vec(environments), current_date, timezone, network, filesystem: None, subagents, } } fn new_with_environments( environments: EnvironmentContextEnvironments, current_date: Option<String>, timezone: Option<String>, network: Option<NetworkContext>, filesystem: Option<FileSystemContext>, subagents: Option<String>, ) -> Self { Self { environments, current_date, timezone, network, filesystem, subagents, } } /// Compares two environment contexts, ignoring the shell. Useful when /// comparing turn to turn, since the initial environment_context will /// include the shell, and then it is not configurable from turn to turn. pub(crate) fn equals_except_shell(&self, other: &EnvironmentContext) -> bool { self.environments.equals_except_shell(&other.environments) && self.current_date == other.current_date && self.timezone == other.timezone && self.network == other.network && self.filesystem == other.filesystem && self.subagents == other.subagents } pub(crate) fn diff_from_turn_context_item( before: &TurnContextItem, after: &EnvironmentContext, ) -> Self { let before_network = Self::network_from_turn_context_item(before); let before_filesystem = Self::filesystem_from_turn_context_item(before); let environments = match &after.environments { EnvironmentContextEnvironments::Single(environment) => { if before.cwd.as_path() != environment.cwd.as_path() { EnvironmentContextEnvironments::Single(EnvironmentContextEnvironment::legacy( environment.cwd.clone(), environment.shell.clone(), )) } else { EnvironmentContextEnvironments::None } } EnvironmentContextEnvironments::Multiple(environments) => { EnvironmentContextEnvironments::Multiple(environments.clone()) } EnvironmentContextEnvironments::None => EnvironmentContextEnvironments::None, }; let network = if before_network != after.network { after.network.clone() } else { before_network }; let filesystem = if before_filesystem != after.filesystem { after.filesystem.clone() } else { before_filesystem }; EnvironmentContext::new_with_environments( environments, after.current_date.clone(), after.timezone.clone(), network, filesystem, /*subagents*/ None, ) } pub(crate) fn from_turn_context(turn_context: &TurnContext, shell: &Shell) -> Self { let mut context = Self::new( EnvironmentContextEnvironment::from_turn_environments( &turn_context.environments.turn_environments, shell, ), turn_context.current_date.clone(), turn_context.timezone.clone(), Self::network_from_turn_context(turn_context), /*subagents*/ None, ); context.filesystem = Some(FileSystemContext::from_permission_profile( &turn_context.permission_profile, &turn_context.config.effective_workspace_roots(), )); context } pub(crate) fn from_turn_context_item( turn_context_item: &TurnContextItem, shell: String, ) -> Self { let cwd = match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) { Ok(cwd) => cwd, Err(_) => AbsolutePathBuf::resolve_path_against_base(&turn_context_item.cwd, "/"), }; Self::new_with_environments( EnvironmentContextEnvironments::from_vec(vec![EnvironmentContextEnvironment::legacy( cwd, shell, )]), turn_context_item.current_date.clone(), turn_context_item.timezone.clone(), Self::network_from_turn_context_item(turn_context_item), Self::filesystem_from_turn_context_item(turn_context_item), /*subagents*/ None, ) } pub(crate) fn with_subagents(mut self, subagents: String) -> Self { if !subagents.is_empty() { self.subagents = Some(subagents); } self } fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContext> { let network = turn_context .config .config_layer_stack .requirements() .network .as_ref()?; Some(NetworkContext::new( network .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains) .unwrap_or_default(), network .domains .as_ref() .and_then(codex_config::NetworkDomainPermissionsToml::denied_domains) .unwrap_or_default(), )) } fn network_from_turn_context_item( turn_context_item: &TurnContextItem, ) -> Option<NetworkContext> { let TurnContextNetworkItem { allowed_domains, denied_domains, } = turn_context_item.network.as_ref()?; Some(NetworkContext::new( allowed_domains.clone(), denied_domains.clone(), )) } fn filesystem_from_turn_context_item( turn_context_item: &TurnContextItem, ) -> Option<FileSystemContext> { Some(FileSystemContext::from_permission_profile( &turn_context_item.permission_profile(), &workspace_roots_from_turn_context_item(turn_context_item), )) }}fn workspace_roots_from_turn_context_item( turn_context_item: &TurnContextItem,) -> Vec<AbsolutePathBuf> { if let Some(workspace_roots) = turn_context_item.workspace_roots.as_ref() { return workspace_roots.clone(); } // Older rollout items did not persist workspace roots. Fall back to the // legacy cwd binding only when reconstructing that historical context. match AbsolutePathBuf::try_from(turn_context_item.cwd.clone()) { Ok(cwd) => vec![cwd], Err(_) => Vec::new(), }}impl ContextualUserFragment for EnvironmentContext { fn role(&self) -> &'static str { "user" } fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() } fn type_markers() -> (&'static str, &'static str) { ( codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG, codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG, ) } fn body(&self) -> String { let mut lines = Vec::new(); match &self.environments { EnvironmentContextEnvironments::Single(environment) => { lines.push(format!( " <cwd>{}</cwd>", environment.cwd.to_string_lossy() )); lines.push(format!(" <shell>{}</shell>", environment.shell)); } EnvironmentContextEnvironments::Multiple(environments) => { lines.push(" <environments>".to_string()); for environment in environments { lines.push(format!(" <environment id=\"{}\">", environment.id)); lines.push(format!( " <cwd>{}</cwd>", environment.cwd.to_string_lossy() )); lines.push(format!(" <shell>{}</shell>", environment.shell)); lines.push(" </environment>".to_string()); } lines.push(" </environments>".to_string()); } EnvironmentContextEnvironments::None => {} } if let Some(current_date) = &self.current_date { lines.push(format!(" <current_date>{current_date}</current_date>")); } if let Some(timezone) = &self.timezone { lines.push(format!(" <timezone>{timezone}</timezone>")); } match &self.network { Some(network) => { lines.push(format!(" {}", network.render())); } None => { // TODO(mbolin): Include this line if it helps the model. // lines.push(" <network enabled=\"false\" />".to_string()); } } if let Some(filesystem) = &self.filesystem { lines.push(format!(" {}", filesystem.render())); } if let Some(subagents) = &self.subagents { lines.push(" <subagents>".to_string()); lines.extend(subagents.lines().map(|line| format!(" {line}"))); lines.push(" </subagents>".to_string()); } format!("\n{}\n", lines.join("\n")) }}#[cfg(test)]#[path = "environment_context_tests.rs"]mod tests;