mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
303 lines
10 KiB
Rust
303 lines
10 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::io;
|
|
use std::path::Path;
|
|
|
|
use codex_network_proxy::NetworkMode;
|
|
use codex_network_proxy::NetworkProxyConfig;
|
|
use codex_protocol::protocol::FileSystemAccessMode;
|
|
use codex_protocol::protocol::FileSystemPath;
|
|
use codex_protocol::protocol::FileSystemSandboxEntry;
|
|
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
|
use codex_protocol::protocol::FileSystemSpecialPath;
|
|
use codex_protocol::protocol::FileSystemSpecialPathKind;
|
|
use codex_protocol::protocol::NetworkSandboxPolicy;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
pub struct PermissionsToml {
|
|
#[serde(flatten)]
|
|
pub entries: BTreeMap<String, PermissionProfileToml>,
|
|
}
|
|
|
|
impl PermissionsToml {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct PermissionProfileToml {
|
|
pub extends: Option<String>,
|
|
pub filesystem: Option<FilesystemPermissionsToml>,
|
|
pub network: Option<NetworkToml>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
pub struct FilesystemPermissionsToml {
|
|
#[serde(flatten)]
|
|
pub entries: BTreeMap<String, FilesystemPermissionToml>,
|
|
}
|
|
|
|
impl FilesystemPermissionsToml {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entries.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
|
#[serde(untagged)]
|
|
pub enum FilesystemPermissionToml {
|
|
Access(FileSystemAccessMode),
|
|
Scoped(BTreeMap<String, FileSystemAccessMode>),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct NetworkToml {
|
|
pub enabled: Option<bool>,
|
|
pub proxy_url: Option<String>,
|
|
pub admin_url: Option<String>,
|
|
pub enable_socks5: Option<bool>,
|
|
pub socks_url: Option<String>,
|
|
pub enable_socks5_udp: Option<bool>,
|
|
pub allow_upstream_proxy: Option<bool>,
|
|
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
|
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
|
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
|
#[schemars(with = "Option<NetworkModeSchema>")]
|
|
pub mode: Option<NetworkMode>,
|
|
pub allowed_domains: Option<Vec<String>>,
|
|
pub denied_domains: Option<Vec<String>>,
|
|
pub allow_unix_sockets: Option<Vec<String>>,
|
|
pub allow_local_binding: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
enum NetworkModeSchema {
|
|
Limited,
|
|
Full,
|
|
}
|
|
|
|
impl NetworkToml {
|
|
pub(crate) fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) {
|
|
if let Some(enabled) = self.enabled {
|
|
config.network.enabled = enabled;
|
|
}
|
|
if let Some(proxy_url) = self.proxy_url.as_ref() {
|
|
config.network.proxy_url = proxy_url.clone();
|
|
}
|
|
if let Some(admin_url) = self.admin_url.as_ref() {
|
|
config.network.admin_url = admin_url.clone();
|
|
}
|
|
if let Some(enable_socks5) = self.enable_socks5 {
|
|
config.network.enable_socks5 = enable_socks5;
|
|
}
|
|
if let Some(socks_url) = self.socks_url.as_ref() {
|
|
config.network.socks_url = socks_url.clone();
|
|
}
|
|
if let Some(enable_socks5_udp) = self.enable_socks5_udp {
|
|
config.network.enable_socks5_udp = enable_socks5_udp;
|
|
}
|
|
if let Some(allow_upstream_proxy) = self.allow_upstream_proxy {
|
|
config.network.allow_upstream_proxy = allow_upstream_proxy;
|
|
}
|
|
if let Some(dangerously_allow_non_loopback_proxy) =
|
|
self.dangerously_allow_non_loopback_proxy
|
|
{
|
|
config.network.dangerously_allow_non_loopback_proxy =
|
|
dangerously_allow_non_loopback_proxy;
|
|
}
|
|
if let Some(dangerously_allow_non_loopback_admin) =
|
|
self.dangerously_allow_non_loopback_admin
|
|
{
|
|
config.network.dangerously_allow_non_loopback_admin =
|
|
dangerously_allow_non_loopback_admin;
|
|
}
|
|
if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets {
|
|
config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;
|
|
}
|
|
if let Some(mode) = self.mode {
|
|
config.network.mode = mode;
|
|
}
|
|
if let Some(allowed_domains) = self.allowed_domains.as_ref() {
|
|
config.network.allowed_domains = allowed_domains.clone();
|
|
}
|
|
if let Some(denied_domains) = self.denied_domains.as_ref() {
|
|
config.network.denied_domains = denied_domains.clone();
|
|
}
|
|
if let Some(allow_unix_sockets) = self.allow_unix_sockets.as_ref() {
|
|
config.network.allow_unix_sockets = allow_unix_sockets.clone();
|
|
}
|
|
if let Some(allow_local_binding) = self.allow_local_binding {
|
|
config.network.allow_local_binding = allow_local_binding;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn to_network_proxy_config(&self) -> NetworkProxyConfig {
|
|
let mut config = NetworkProxyConfig::default();
|
|
self.apply_to_network_proxy_config(&mut config);
|
|
config
|
|
}
|
|
}
|
|
|
|
pub(crate) fn network_proxy_config_from_network(
|
|
network: Option<&NetworkToml>,
|
|
) -> NetworkProxyConfig {
|
|
network.map_or_else(
|
|
NetworkProxyConfig::default,
|
|
NetworkToml::to_network_proxy_config,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn compile_permission_profile(
|
|
permissions: &PermissionsToml,
|
|
profile_name: &str,
|
|
) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> {
|
|
let profile = permissions.entries.get(profile_name).ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("default_permissions refers to undefined profile `{profile_name}`"),
|
|
)
|
|
})?;
|
|
|
|
if profile.extends.is_some() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"permissions profile `{profile_name}` uses `extends`, which is not supported yet"
|
|
),
|
|
));
|
|
}
|
|
|
|
let filesystem = profile.filesystem.as_ref().ok_or_else(|| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"permissions profile `{profile_name}` must define a `[permissions.{profile_name}.filesystem]` table"
|
|
),
|
|
)
|
|
})?;
|
|
|
|
if filesystem.is_empty() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"permissions profile `{profile_name}` must define at least one filesystem entry"
|
|
),
|
|
));
|
|
}
|
|
|
|
let mut entries = Vec::new();
|
|
for (path, permission) in &filesystem.entries {
|
|
compile_filesystem_permission(path, permission, &mut entries)?;
|
|
}
|
|
|
|
Ok((
|
|
FileSystemSandboxPolicy::restricted(entries),
|
|
match profile.network.as_ref().and_then(|network| network.enabled) {
|
|
Some(true) => NetworkSandboxPolicy::Enabled,
|
|
_ => NetworkSandboxPolicy::Restricted,
|
|
},
|
|
))
|
|
}
|
|
|
|
fn compile_filesystem_permission(
|
|
path: &str,
|
|
permission: &FilesystemPermissionToml,
|
|
entries: &mut Vec<FileSystemSandboxEntry>,
|
|
) -> io::Result<()> {
|
|
match permission {
|
|
FilesystemPermissionToml::Access(access) => entries.push(FileSystemSandboxEntry {
|
|
path: compile_filesystem_path(path)?,
|
|
access: *access,
|
|
}),
|
|
FilesystemPermissionToml::Scoped(scoped_entries) => {
|
|
for (subpath, access) in scoped_entries {
|
|
entries.push(FileSystemSandboxEntry {
|
|
path: compile_scoped_filesystem_path(path, subpath)?,
|
|
access: *access,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn compile_filesystem_path(path: &str) -> io::Result<FileSystemPath> {
|
|
if let Some(special) = parse_special_path(path)? {
|
|
return Ok(FileSystemPath::Special { value: special });
|
|
}
|
|
|
|
let path = parse_absolute_path(path)?;
|
|
Ok(FileSystemPath::Path { path })
|
|
}
|
|
|
|
fn compile_scoped_filesystem_path(path: &str, subpath: &str) -> io::Result<FileSystemPath> {
|
|
if subpath == "." {
|
|
return compile_filesystem_path(path);
|
|
}
|
|
|
|
let subpath = Path::new(subpath);
|
|
if subpath.is_absolute() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"filesystem subpath `{}` must be relative",
|
|
subpath.display()
|
|
),
|
|
));
|
|
}
|
|
|
|
if let Some(mut special) = parse_special_path(path)? {
|
|
if !matches!(special.kind, FileSystemSpecialPathKind::ProjectRoots) {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("filesystem path `{path}` does not support nested entries"),
|
|
));
|
|
}
|
|
special.subpath = Some(subpath.to_path_buf());
|
|
return Ok(FileSystemPath::Special { value: special });
|
|
}
|
|
|
|
let base = parse_absolute_path(path)?;
|
|
let path = AbsolutePathBuf::resolve_path_against_base(subpath, base.as_path())?;
|
|
Ok(FileSystemPath::Path { path })
|
|
}
|
|
|
|
fn parse_special_path(path: &str) -> io::Result<Option<FileSystemSpecialPath>> {
|
|
let kind = match path {
|
|
":root" => Some(FileSystemSpecialPathKind::Root),
|
|
":minimal" => Some(FileSystemSpecialPathKind::Minimal),
|
|
":project_roots" => Some(FileSystemSpecialPathKind::ProjectRoots),
|
|
":tmpdir" => Some(FileSystemSpecialPathKind::Tmpdir),
|
|
_ if path.starts_with(':') => {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("unknown filesystem special path `{path}`"),
|
|
));
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
Ok(kind.map(|kind| FileSystemSpecialPath {
|
|
kind,
|
|
subpath: None,
|
|
}))
|
|
}
|
|
|
|
fn parse_absolute_path(path: &str) -> io::Result<AbsolutePathBuf> {
|
|
let path_ref = Path::new(path);
|
|
if !path_ref.is_absolute() && path != "~" && !path.starts_with("~/") {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::InvalidInput,
|
|
format!("filesystem path `{path}` must be absolute, use `~/...`, or start with `:`"),
|
|
));
|
|
}
|
|
AbsolutePathBuf::from_absolute_path(path_ref)
|
|
}
|