mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Merge d75dde3ae5 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -16,7 +16,7 @@ use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_sandboxing::seatbelt::CreateSeatbeltCommandArgsParams;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -222,16 +222,11 @@ async fn run_command_under_sandbox(
|
||||
.codex_linux_sandbox_exe
|
||||
.expect("codex-linux-sandbox executable not found");
|
||||
let use_legacy_landlock = config.features.use_legacy_landlock();
|
||||
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
|
||||
let network_sandbox_policy = config.permissions.network_sandbox_policy();
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
command,
|
||||
cwd.as_path(),
|
||||
&config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(sandbox_policy_cwd.as_path()),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
&config.permissions.permission_profile(),
|
||||
sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
/*allow_network_for_proxy*/ false,
|
||||
|
||||
@@ -186,11 +186,45 @@ pub struct ToolSuggestDiscoverable {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSuggestDisabledTool {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: ToolSuggestDiscoverableType,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl ToolSuggestDisabledTool {
|
||||
pub fn plugin(id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind: ToolSuggestDiscoverableType::Plugin,
|
||||
id: id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connector(id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind: ToolSuggestDiscoverableType::Connector,
|
||||
id: id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalized(&self) -> Option<Self> {
|
||||
let id = self.id.trim();
|
||||
(!id.is_empty()).then(|| Self {
|
||||
kind: self.kind,
|
||||
id: id.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ToolSuggestConfig {
|
||||
#[serde(default)]
|
||||
pub discoverables: Vec<ToolSuggestDiscoverable>,
|
||||
#[serde(default)]
|
||||
pub disabled_tools: Vec<ToolSuggestDisabledTool>,
|
||||
}
|
||||
|
||||
/// Memories settings loaded from config.toml.
|
||||
|
||||
@@ -2169,6 +2169,13 @@
|
||||
"ToolSuggestConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disabled_tools": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/ToolSuggestDisabledTool"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"discoverables": {
|
||||
"default": [],
|
||||
"items": {
|
||||
@@ -2179,6 +2186,22 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestDisabledTool": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/ToolSuggestDiscoverableType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ToolSuggestDiscoverable": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
||||
@@ -46,6 +46,7 @@ use codex_config::types::NotificationMethod;
|
||||
use codex_config::types::Notifications;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_config::types::SkillsConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_config::types::Tui;
|
||||
use codex_config::types::TuiKeymap;
|
||||
@@ -8227,6 +8228,7 @@ discoverables = [
|
||||
id: " ".to_string(),
|
||||
},
|
||||
],
|
||||
disabled_tools: Vec::new(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -8251,11 +8253,118 @@ discoverables = [
|
||||
id: "plugin_alpha@openai-curated".to_string(),
|
||||
},
|
||||
],
|
||||
disabled_tools: Vec::new(),
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_disabled_tools_load_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
r#"
|
||||
[tool_suggest]
|
||||
disabled_tools = [
|
||||
{ type = "connector", id = " connector_calendar " },
|
||||
{ type = "connector", id = "connector_calendar" },
|
||||
{ type = "connector", id = " " },
|
||||
{ type = "plugin", id = "slack@openai-curated" }
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("TOML deserialization should succeed");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tool_suggest,
|
||||
Some(ToolSuggestConfig {
|
||||
discoverables: Vec::new(),
|
||||
disabled_tools: vec![
|
||||
ToolSuggestDisabledTool::connector(" connector_calendar "),
|
||||
ToolSuggestDisabledTool::connector("connector_calendar"),
|
||||
ToolSuggestDisabledTool::connector(" "),
|
||||
ToolSuggestDisabledTool::plugin("slack@openai-curated"),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.tool_suggest,
|
||||
ToolSuggestConfig {
|
||||
discoverables: Vec::new(),
|
||||
disabled_tools: vec![
|
||||
ToolSuggestDisabledTool::connector("connector_calendar"),
|
||||
ToolSuggestDisabledTool::plugin("slack@openai-curated"),
|
||||
],
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_disabled_tools_merge_across_config_layers() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let workspace = TempDir::new()?;
|
||||
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"
|
||||
[projects."{workspace_key}"]
|
||||
trust_level = "trusted"
|
||||
|
||||
[tool_suggest]
|
||||
disabled_tools = [
|
||||
{{ type = "connector", id = " user_connector " }},
|
||||
{{ type = "plugin", id = "shared_plugin" }},
|
||||
{{ type = "connector", id = "project_connector" }},
|
||||
]
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let project_config_dir = workspace.path().join(".codex");
|
||||
std::fs::create_dir_all(&project_config_dir)?;
|
||||
std::fs::write(
|
||||
project_config_dir.join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[tool_suggest]
|
||||
disabled_tools = [
|
||||
{ type = "connector", id = "project_connector" },
|
||||
{ type = "plugin", id = "project_plugin" },
|
||||
{ type = "plugin", id = "shared_plugin" },
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(workspace.path().to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.tool_suggest.disabled_tools,
|
||||
vec![
|
||||
ToolSuggestDisabledTool::connector("user_connector"),
|
||||
ToolSuggestDisabledTool::plugin("shared_plugin"),
|
||||
ToolSuggestDisabledTool::connector("project_connector"),
|
||||
ToolSuggestDisabledTool::plugin("project_plugin"),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_realtime_start_instructions_load_from_config_toml() -> std::io::Result<()> {
|
||||
let cfg: ConfigToml = toml::from_str(
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::path_utils::write_atomically;
|
||||
use anyhow::Context;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_features::FEATURES;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
@@ -10,6 +11,7 @@ use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::task;
|
||||
@@ -57,6 +59,8 @@ pub enum ConfigEdit {
|
||||
RecordModelMigrationSeen { from: String, to: String },
|
||||
/// Replace the entire `[mcp_servers]` table.
|
||||
ReplaceMcpServers(BTreeMap<String, McpServerConfig>),
|
||||
/// Add a disabled tool suggestion under `[tool_suggest].disabled_tools`.
|
||||
AddToolSuggestDisabledTool(ToolSuggestDisabledTool),
|
||||
/// Set or clear a skill config entry under `[[skills.config]]` by path.
|
||||
SetSkillConfig { path: PathBuf, enabled: bool },
|
||||
/// Set or clear a skill config entry under `[[skills.config]]` by name.
|
||||
@@ -180,10 +184,13 @@ mod document_helpers {
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use toml_edit::Array as TomlArray;
|
||||
use toml_edit::InlineTable;
|
||||
use toml_edit::Item as TomlItem;
|
||||
use toml_edit::Table as TomlTable;
|
||||
use toml_edit::Value as TomlValue;
|
||||
use toml_edit::value;
|
||||
|
||||
pub(super) fn ensure_table_for_write(item: &mut TomlItem) -> Option<&mut TomlTable> {
|
||||
@@ -379,6 +386,57 @@ mod document_helpers {
|
||||
table
|
||||
}
|
||||
|
||||
pub(super) fn parse_tool_suggest_disabled_tool(
|
||||
value: &TomlValue,
|
||||
) -> Option<ToolSuggestDisabledTool> {
|
||||
let table = value.as_inline_table()?;
|
||||
let kind = match table.get("type").and_then(TomlValue::as_str) {
|
||||
Some("connector") => ToolSuggestDiscoverableType::Connector,
|
||||
Some("plugin") => ToolSuggestDiscoverableType::Plugin,
|
||||
_ => return None,
|
||||
};
|
||||
let id = table.get("id").and_then(TomlValue::as_str)?;
|
||||
Some(ToolSuggestDisabledTool {
|
||||
kind,
|
||||
id: id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn parse_tool_suggest_disabled_tool_table(
|
||||
table: &TomlTable,
|
||||
) -> Option<ToolSuggestDisabledTool> {
|
||||
let kind = match table.get("type").and_then(TomlItem::as_str) {
|
||||
Some("connector") => ToolSuggestDiscoverableType::Connector,
|
||||
Some("plugin") => ToolSuggestDiscoverableType::Plugin,
|
||||
_ => return None,
|
||||
};
|
||||
let id = table.get("id").and_then(TomlItem::as_str)?;
|
||||
Some(ToolSuggestDisabledTool {
|
||||
kind,
|
||||
id: id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn tool_suggest_disabled_tools_value(
|
||||
disabled_tools: &[ToolSuggestDisabledTool],
|
||||
) -> TomlItem {
|
||||
let mut array = TomlArray::new();
|
||||
for disabled_tool in disabled_tools {
|
||||
let mut table = InlineTable::new();
|
||||
table.insert(
|
||||
"type",
|
||||
match disabled_tool.kind {
|
||||
ToolSuggestDiscoverableType::Connector => "connector",
|
||||
ToolSuggestDiscoverableType::Plugin => "plugin",
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
table.insert("id", disabled_tool.id.clone().into());
|
||||
array.push(table);
|
||||
}
|
||||
TomlItem::Value(array.into())
|
||||
}
|
||||
|
||||
fn array_from_iter<I>(iter: I) -> TomlItem
|
||||
where
|
||||
I: Iterator<Item = String>,
|
||||
@@ -552,6 +610,9 @@ impl ConfigDocument {
|
||||
value(*acknowledged),
|
||||
)),
|
||||
ConfigEdit::ReplaceMcpServers(servers) => Ok(self.replace_mcp_servers(servers)),
|
||||
ConfigEdit::AddToolSuggestDisabledTool(disabled_tool) => {
|
||||
Ok(self.add_tool_suggest_disabled_tool(disabled_tool))
|
||||
}
|
||||
ConfigEdit::SetSkillConfig { path, enabled } => {
|
||||
Ok(self.set_skill_config(SkillConfigSelector::Path(path.clone()), *enabled))
|
||||
}
|
||||
@@ -590,6 +651,41 @@ impl ConfigDocument {
|
||||
self.remove(&resolved)
|
||||
}
|
||||
|
||||
fn add_tool_suggest_disabled_tool(&mut self, disabled_tool: &ToolSuggestDisabledTool) -> bool {
|
||||
let disabled_tools_item = self
|
||||
.doc
|
||||
.get("tool_suggest")
|
||||
.and_then(|item| item.as_table_like())
|
||||
.and_then(|table| table.get("disabled_tools"));
|
||||
let existing_from_array = disabled_tools_item
|
||||
.and_then(|item| item.as_value())
|
||||
.and_then(|value| value.as_array())
|
||||
.into_iter()
|
||||
.flat_map(|array| array.iter())
|
||||
.filter_map(document_helpers::parse_tool_suggest_disabled_tool);
|
||||
let existing_from_tables = disabled_tools_item
|
||||
.and_then(|item| match item {
|
||||
TomlItem::ArrayOfTables(array) => Some(array),
|
||||
_ => None,
|
||||
})
|
||||
.into_iter()
|
||||
.flat_map(|array| array.iter())
|
||||
.filter_map(document_helpers::parse_tool_suggest_disabled_tool_table);
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let disabled_tools = existing_from_array
|
||||
.chain(existing_from_tables)
|
||||
.chain(std::iter::once(disabled_tool.clone()))
|
||||
.filter_map(|disabled_tool| disabled_tool.normalized())
|
||||
.filter(|disabled_tool| seen.insert(disabled_tool.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
self.write_value(
|
||||
Scope::Global,
|
||||
&["tool_suggest", "disabled_tools"],
|
||||
document_helpers::tool_suggest_disabled_tools_value(&disabled_tools),
|
||||
)
|
||||
}
|
||||
|
||||
fn clear_owned(&mut self, segments: &[String]) -> bool {
|
||||
self.remove(segments)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ use codex_config::types::OtelConfig;
|
||||
use codex_config::types::OtelConfigToml;
|
||||
use codex_config::types::OtelExporterKind;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
use codex_config::types::TuiKeymap;
|
||||
use codex_config::types::TuiNotificationSettings;
|
||||
@@ -96,6 +97,7 @@ use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -1440,10 +1442,29 @@ pub struct AgentRoleConfig {
|
||||
pub nickname_candidates: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig {
|
||||
let discoverables = config_toml
|
||||
.tool_suggest
|
||||
.as_ref()
|
||||
fn resolve_tool_suggest_config(
|
||||
config_toml: &ConfigToml,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> ToolSuggestConfig {
|
||||
resolve_tool_suggest_config_from_config(config_toml.tool_suggest.as_ref(), config_layer_stack)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_tool_suggest_config_from_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> ToolSuggestConfig {
|
||||
let tool_suggest = config_layer_stack
|
||||
.effective_config()
|
||||
.get("tool_suggest")
|
||||
.cloned()
|
||||
.and_then(|value| value.try_into::<ToolSuggestConfig>().ok());
|
||||
resolve_tool_suggest_config_from_config(tool_suggest.as_ref(), config_layer_stack)
|
||||
}
|
||||
|
||||
fn resolve_tool_suggest_config_from_config(
|
||||
tool_suggest: Option<&ToolSuggestConfig>,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> ToolSuggestConfig {
|
||||
let discoverables = tool_suggest
|
||||
.into_iter()
|
||||
.flat_map(|tool_suggest| tool_suggest.discoverables.iter())
|
||||
.filter_map(|discoverable| {
|
||||
@@ -1458,8 +1479,47 @@ fn resolve_tool_suggest_config(config_toml: &ConfigToml) -> ToolSuggestConfig {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut seen_disabled_tools = HashSet::new();
|
||||
let mut disabled_tools = Vec::new();
|
||||
let mut add_disabled_tool = |disabled_tool: ToolSuggestDisabledTool| {
|
||||
if let Some(disabled_tool) = disabled_tool.normalized()
|
||||
&& seen_disabled_tools.insert(disabled_tool.clone())
|
||||
{
|
||||
disabled_tools.push(disabled_tool);
|
||||
}
|
||||
};
|
||||
|
||||
ToolSuggestConfig { discoverables }
|
||||
let layers = config_layer_stack.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
);
|
||||
if layers.is_empty() {
|
||||
for disabled_tool in tool_suggest
|
||||
.into_iter()
|
||||
.flat_map(|tool_suggest| tool_suggest.disabled_tools.iter().cloned())
|
||||
{
|
||||
add_disabled_tool(disabled_tool);
|
||||
}
|
||||
} else {
|
||||
for layer in layers {
|
||||
let Some(tool_suggest) = layer
|
||||
.config
|
||||
.get("tool_suggest")
|
||||
.cloned()
|
||||
.and_then(|value| value.try_into::<ToolSuggestConfig>().ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for disabled_tool in tool_suggest.disabled_tools {
|
||||
add_disabled_tool(disabled_tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolSuggestConfig {
|
||||
discoverables,
|
||||
disabled_tools,
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_store_config(
|
||||
@@ -1864,7 +1924,7 @@ impl Config {
|
||||
.clone(),
|
||||
None => ConfigProfile::default(),
|
||||
};
|
||||
let tool_suggest = resolve_tool_suggest_config(&cfg);
|
||||
let tool_suggest = resolve_tool_suggest_config(&cfg, &config_layer_stack);
|
||||
let feature_overrides = FeatureOverrides {
|
||||
include_apply_patch_tool: include_apply_patch_tool_override,
|
||||
web_search_request: override_tools_web_search_request,
|
||||
|
||||
@@ -419,6 +419,14 @@ async fn tool_suggest_connector_ids(config: &Config) -> HashSet<String> {
|
||||
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Connector)
|
||||
.map(|discoverable| discoverable.id.clone()),
|
||||
);
|
||||
let disabled_connector_ids = config
|
||||
.tool_suggest
|
||||
.disabled_tools
|
||||
.iter()
|
||||
.filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Connector)
|
||||
.map(|disabled_tool| disabled_tool.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
connector_ids.retain(|connector_id| !disabled_connector_ids.contains(connector_id.as_str()));
|
||||
connector_ids
|
||||
}
|
||||
|
||||
|
||||
@@ -1112,6 +1112,35 @@ discoverables = [
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tool_suggest_connector_ids_exclude_disabled_tool_suggestions() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[tool_suggest]
|
||||
discoverables = [
|
||||
{ type = "connector", id = "connector_calendar" },
|
||||
{ type = "connector", id = "connector_gmail" }
|
||||
]
|
||||
disabled_tools = [
|
||||
{ type = "connector", id = "connector_calendar" }
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("config should load");
|
||||
|
||||
assert_eq!(
|
||||
tool_suggest_connector_ids(&config).await,
|
||||
HashSet::from(["connector_gmail".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_tool_suggest_discoverable_connectors_keeps_only_plugin_backed_uninstalled_apps() {
|
||||
let filtered = filter_tool_suggest_discoverable_connectors(
|
||||
|
||||
@@ -3,10 +3,9 @@ use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_sandboxing::landlock::allow_network_for_proxy;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies;
|
||||
use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
@@ -17,9 +16,8 @@ use tokio::process::Child;
|
||||
/// isolation plus seccomp for network restrictions.
|
||||
///
|
||||
/// Unlike macOS Seatbelt where we directly embed the policy text, the Linux
|
||||
/// helper is a separate executable. We pass both the canonical split
|
||||
/// filesystem/network policies and a compatibility legacy projection as JSON
|
||||
/// until the helper protocol no longer needs the legacy field.
|
||||
/// helper is a separate executable. We pass the canonical permission profile
|
||||
/// as JSON and let the helper derive the runtime filesystem/network policies.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn spawn_command_under_linux_sandbox<P>(
|
||||
codex_linux_sandbox_exe: P,
|
||||
@@ -35,20 +33,11 @@ pub async fn spawn_command_under_linux_sandbox<P>(
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
let network_sandbox_policy = permission_profile.network_sandbox_policy();
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
command,
|
||||
command_cwd.as_path(),
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
use_legacy_landlock,
|
||||
allow_network_for_proxy(/*enforce_managed_network*/ false),
|
||||
|
||||
@@ -43,6 +43,13 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins(
|
||||
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Plugin)
|
||||
.map(|discoverable| discoverable.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let disabled_plugin_ids = config
|
||||
.tool_suggest
|
||||
.disabled_tools
|
||||
.iter()
|
||||
.filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Plugin)
|
||||
.map(|disabled_tool| disabled_tool.id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let marketplaces = plugins_manager
|
||||
.list_marketplaces_for_config(config, &[])
|
||||
.context("failed to list plugin marketplaces for tool suggestions")?
|
||||
@@ -56,6 +63,7 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins(
|
||||
|
||||
for plugin in marketplace.plugins {
|
||||
if plugin.installed
|
||||
|| disabled_plugin_ids.contains(plugin.id.as_str())
|
||||
|| (!TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str())
|
||||
&& !configured_plugin_ids.contains(plugin.id.as_str()))
|
||||
{
|
||||
|
||||
@@ -230,6 +230,31 @@ async fn list_tool_suggest_discoverable_plugins_omits_installed_curated_plugins(
|
||||
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tool_suggest_discoverable_plugins_omits_disabled_tool_suggestions() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let curated_root = curated_plugins_repo_path(codex_home.path());
|
||||
write_openai_curated_marketplace(&curated_root, &["slack"]);
|
||||
write_file(
|
||||
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
|
||||
[tool_suggest]
|
||||
disabled_tools = [
|
||||
{ type = "plugin", id = "slack@openai-curated" }
|
||||
]
|
||||
"#,
|
||||
);
|
||||
|
||||
let config = load_plugins_config(codex_home.path()).await;
|
||||
let discoverable_plugins = list_tool_suggest_discoverable_plugins(&config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(discoverable_plugins, Vec::<DiscoverablePluginInfo>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tool_suggest_discoverable_plugins_includes_configured_plugin_ids() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::build_available_skills;
|
||||
use crate::commit_attribution::commit_message_trailer_instruction;
|
||||
use crate::compact;
|
||||
use crate::config::ManagedFeatures;
|
||||
use crate::config::resolve_tool_suggest_config_from_layer_stack;
|
||||
use crate::connectors;
|
||||
use crate::context::ApprovedCommandPrefixSaved;
|
||||
use crate::context::AppsInstructions;
|
||||
@@ -1409,6 +1410,8 @@ impl Session {
|
||||
config.config_layer_stack = config
|
||||
.config_layer_stack
|
||||
.with_user_config(&config_toml_path, user_config);
|
||||
config.tool_suggest =
|
||||
resolve_tool_suggest_config_from_layer_stack(&config.config_layer_stack);
|
||||
state.session_configuration.original_config_do_not_use = Arc::new(config);
|
||||
self.services.skills_manager.clear_cache();
|
||||
self.services.plugins_manager.clear_cache();
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_config::NetworkDomainPermissionsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
@@ -1155,6 +1156,35 @@ async fn reload_user_config_layer_updates_effective_apps_config() {
|
||||
assert_eq!(app.destructive_enabled, Some(false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reload_user_config_layer_updates_effective_tool_suggest_config() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let codex_home = session.codex_home().await;
|
||||
std::fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
let config_toml_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
std::fs::write(
|
||||
&config_toml_path,
|
||||
r#"[tool_suggest]
|
||||
disabled_tools = [
|
||||
{ type = "connector", id = " calendar " },
|
||||
{ type = "plugin", id = "slack@openai-curated" },
|
||||
]
|
||||
"#,
|
||||
)
|
||||
.expect("write user config");
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
|
||||
let config = session.get_config().await;
|
||||
assert_eq!(
|
||||
config.tool_suggest.disabled_tools,
|
||||
vec![
|
||||
ToolSuggestDisabledTool::connector("calendar"),
|
||||
ToolSuggestDisabledTool::plugin("slack@openai-curated"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_connectors_for_input_skips_duplicate_slug_mentions() {
|
||||
let connectors = vec![
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::DiscoverableToolAction;
|
||||
use codex_tools::DiscoverableToolType;
|
||||
use codex_tools::TOOL_SUGGEST_PERSIST_ALWAYS_VALUE;
|
||||
use codex_tools::TOOL_SUGGEST_PERSIST_KEY;
|
||||
use codex_tools::TOOL_SUGGEST_TOOL_NAME;
|
||||
use codex_tools::ToolSuggestArgs;
|
||||
use codex_tools::ToolSuggestResult;
|
||||
@@ -14,8 +18,11 @@ use codex_tools::build_tool_suggestion_elicitation_request;
|
||||
use codex_tools::filter_tool_suggest_discoverable_tools_for_client;
|
||||
use codex_tools::verified_connector_suggestion_completed;
|
||||
use rmcp::model::RequestId;
|
||||
use serde_json::Value;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::connectors;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
@@ -123,6 +130,9 @@ impl ToolHandler for ToolSuggestHandler {
|
||||
let response = session
|
||||
.request_mcp_server_elicitation(turn.as_ref(), request_id, params)
|
||||
.await;
|
||||
if let Some(response) = response.as_ref() {
|
||||
maybe_persist_tool_suggest_disable(&session, &turn, &tool, response).await;
|
||||
}
|
||||
let user_confirmed = response
|
||||
.as_ref()
|
||||
.is_some_and(|response| response.action == ElicitationAction::Accept);
|
||||
@@ -158,6 +168,63 @@ impl ToolHandler for ToolSuggestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async fn maybe_persist_tool_suggest_disable(
|
||||
session: &crate::session::session::Session,
|
||||
turn: &crate::session::turn_context::TurnContext,
|
||||
tool: &DiscoverableTool,
|
||||
response: &ElicitationResponse,
|
||||
) {
|
||||
if !tool_suggest_response_requests_persistent_disable(response) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = persist_tool_suggest_disable(&turn.config.codex_home, tool).await {
|
||||
warn!(
|
||||
error = %err,
|
||||
tool_id = tool.id(),
|
||||
"failed to persist disabled tool suggestion"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
session.reload_user_config_layer().await;
|
||||
}
|
||||
|
||||
fn tool_suggest_response_requests_persistent_disable(response: &ElicitationResponse) -> bool {
|
||||
if response.action != ElicitationAction::Decline {
|
||||
return false;
|
||||
}
|
||||
|
||||
response
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|meta| meta.get(TOOL_SUGGEST_PERSIST_KEY))
|
||||
.and_then(Value::as_str)
|
||||
== Some(TOOL_SUGGEST_PERSIST_ALWAYS_VALUE)
|
||||
}
|
||||
|
||||
async fn persist_tool_suggest_disable(
|
||||
codex_home: &codex_utils_absolute_path::AbsolutePathBuf,
|
||||
tool: &DiscoverableTool,
|
||||
) -> anyhow::Result<()> {
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([ConfigEdit::AddToolSuggestDisabledTool(
|
||||
disabled_tool_suggestion(tool),
|
||||
)])
|
||||
.apply()
|
||||
.await
|
||||
}
|
||||
|
||||
fn disabled_tool_suggestion(tool: &DiscoverableTool) -> ToolSuggestDisabledTool {
|
||||
match tool {
|
||||
DiscoverableTool::Connector(connector) => {
|
||||
ToolSuggestDisabledTool::connector(connector.id.as_str())
|
||||
}
|
||||
DiscoverableTool::Plugin(plugin) => ToolSuggestDisabledTool::plugin(plugin.id.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_tool_suggestion_completed(
|
||||
session: &crate::session::session::Session,
|
||||
turn: &crate::session::turn_context::TurnContext,
|
||||
|
||||
@@ -5,8 +5,20 @@ use crate::plugins::test_support::load_plugins_config;
|
||||
use crate::plugins::test_support::write_curated_plugin_sha;
|
||||
use crate::plugins::test_support::write_openai_curated_marketplace;
|
||||
use crate::plugins::test_support::write_plugins_feature_config;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_tools::DiscoverablePluginInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use serde_json::json;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -44,3 +56,155 @@ async fn verified_plugin_suggestion_completed_requires_installed_plugin() {
|
||||
&plugins_manager,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_suggest_response_persists_only_decline_always_mode() {
|
||||
assert!(tool_suggest_response_requests_persistent_disable(
|
||||
&ElicitationResponse {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: Some(json!({ TOOL_SUGGEST_PERSIST_KEY: TOOL_SUGGEST_PERSIST_ALWAYS_VALUE })),
|
||||
}
|
||||
));
|
||||
assert!(!tool_suggest_response_requests_persistent_disable(
|
||||
&ElicitationResponse {
|
||||
action: ElicitationAction::Accept,
|
||||
content: None,
|
||||
meta: Some(json!({ TOOL_SUGGEST_PERSIST_KEY: TOOL_SUGGEST_PERSIST_ALWAYS_VALUE })),
|
||||
}
|
||||
));
|
||||
assert!(!tool_suggest_response_requests_persistent_disable(
|
||||
&ElicitationResponse {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: Some(json!({ TOOL_SUGGEST_PERSIST_KEY: "session" })),
|
||||
}
|
||||
));
|
||||
assert!(!tool_suggest_response_requests_persistent_disable(
|
||||
&ElicitationResponse {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn persist_tool_suggest_disable_writes_connector_config() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let tool = connector_tool("connector_calendar", "Google Calendar");
|
||||
|
||||
persist_tool_suggest_disable(&codex_home.path().abs(), &tool)
|
||||
.await
|
||||
.expect("persist connector disable");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let parsed: ConfigToml = toml::from_str(&contents).expect("parse config");
|
||||
assert_eq!(
|
||||
parsed.tool_suggest,
|
||||
Some(ToolSuggestConfig {
|
||||
discoverables: Vec::new(),
|
||||
disabled_tools: vec![ToolSuggestDisabledTool::connector("connector_calendar")],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn persist_tool_suggest_disable_writes_plugin_config() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let tool = DiscoverableTool::Plugin(Box::new(DiscoverablePluginInfo {
|
||||
id: "slack@openai-curated".to_string(),
|
||||
name: "Slack".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}));
|
||||
|
||||
persist_tool_suggest_disable(&codex_home.path().abs(), &tool)
|
||||
.await
|
||||
.expect("persist plugin disable");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let parsed: ConfigToml = toml::from_str(&contents).expect("parse config");
|
||||
assert_eq!(
|
||||
parsed.tool_suggest,
|
||||
Some(ToolSuggestConfig {
|
||||
discoverables: Vec::new(),
|
||||
disabled_tools: vec![ToolSuggestDisabledTool::plugin("slack@openai-curated")],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn persist_tool_suggest_disable_dedupes_existing_disabled_tools() {
|
||||
let codex_home = tempdir().expect("tempdir should succeed");
|
||||
let tool = connector_tool("connector_calendar", "Google Calendar");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[tool_suggest]
|
||||
discoverables = [
|
||||
{ type = "plugin", id = "sample@openai-curated" }
|
||||
]
|
||||
|
||||
[[tool_suggest.disabled_tools]]
|
||||
type = "connector"
|
||||
id = " connector_calendar "
|
||||
|
||||
[[tool_suggest.disabled_tools]]
|
||||
type = "connector"
|
||||
id = "connector_calendar"
|
||||
|
||||
[[tool_suggest.disabled_tools]]
|
||||
type = "connector"
|
||||
id = " "
|
||||
|
||||
[[tool_suggest.disabled_tools]]
|
||||
type = "plugin"
|
||||
id = "slack@openai-curated"
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
persist_tool_suggest_disable(&codex_home.path().abs(), &tool)
|
||||
.await
|
||||
.expect("persist connector disable");
|
||||
|
||||
let contents =
|
||||
std::fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let parsed: ConfigToml = toml::from_str(&contents).expect("parse config");
|
||||
assert_eq!(
|
||||
parsed.tool_suggest,
|
||||
Some(ToolSuggestConfig {
|
||||
discoverables: vec![ToolSuggestDiscoverable {
|
||||
kind: ToolSuggestDiscoverableType::Plugin,
|
||||
id: "sample@openai-curated".to_string(),
|
||||
}],
|
||||
disabled_tools: vec![
|
||||
ToolSuggestDisabledTool::connector("connector_calendar"),
|
||||
ToolSuggestDisabledTool::plugin("slack@openai-curated"),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
fn connector_tool(id: &str, name: &str) -> DiscoverableTool {
|
||||
DiscoverableTool::Connector(Box::new(AppInfo {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1036,7 +1036,6 @@ mod tests {
|
||||
use codex_protocol::protocol::FileSystemSandboxEntry;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
@@ -1066,7 +1065,7 @@ mod tests {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
command.clone(),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
@@ -1085,7 +1084,7 @@ mod tests {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
command,
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
@@ -1399,22 +1398,18 @@ mod tests {
|
||||
let missing_root = temp_dir.path().join("missing");
|
||||
std::fs::create_dir(&existing_root).expect("create existing root");
|
||||
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![
|
||||
let policy = FileSystemSandboxPolicy::workspace_write(
|
||||
&[
|
||||
AbsolutePathBuf::try_from(existing_root.as_path()).expect("absolute existing root"),
|
||||
AbsolutePathBuf::try_from(missing_root.as_path()).expect("absolute missing root"),
|
||||
],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
|
||||
let args = create_filesystem_args(
|
||||
&FileSystemSandboxPolicy::from(&policy),
|
||||
temp_dir.path(),
|
||||
NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH,
|
||||
)
|
||||
.expect("filesystem args");
|
||||
let args =
|
||||
create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH)
|
||||
.expect("filesystem args");
|
||||
let existing_root = path_to_string(&existing_root);
|
||||
let missing_root = path_to_string(&missing_root);
|
||||
|
||||
@@ -1532,15 +1527,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mounts_dev_before_writable_dev_binds() {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::try_from(Path::new("/dev")).expect("/dev path")],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let sandbox_policy = FileSystemSandboxPolicy::workspace_write(
|
||||
&[AbsolutePathBuf::try_from(Path::new("/dev")).expect("/dev path")],
|
||||
/*exclude_tmpdir_env_var*/ true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
|
||||
let args = create_filesystem_args(
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&sandbox_policy,
|
||||
Path::new("/"),
|
||||
NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH,
|
||||
)
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::path::Path;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use landlock::ABI;
|
||||
@@ -39,14 +39,15 @@ use seccompiler::apply_filter;
|
||||
/// - installing the network seccomp filter when network access is disabled.
|
||||
///
|
||||
/// Filesystem restrictions are intentionally handled by bubblewrap.
|
||||
pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
pub(crate) fn apply_permission_profile_to_current_thread(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
apply_landlock_fs: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_routed_network: bool,
|
||||
) -> Result<()> {
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let network_seccomp_mode = network_seccomp_mode(
|
||||
network_sandbox_policy,
|
||||
allow_network_for_proxy,
|
||||
@@ -58,7 +59,7 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||||
// we avoid this unless we need seccomp or we are explicitly using the
|
||||
// legacy Landlock filesystem pipeline.
|
||||
if network_seccomp_mode.is_some()
|
||||
|| (apply_landlock_fs && !sandbox_policy.has_full_disk_write_access())
|
||||
|| (apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access())
|
||||
{
|
||||
set_no_new_privs()?;
|
||||
}
|
||||
@@ -67,15 +68,15 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||||
install_network_seccomp_filter_on_current_thread(mode)?;
|
||||
}
|
||||
|
||||
if apply_landlock_fs && !sandbox_policy.has_full_disk_write_access() {
|
||||
if !sandbox_policy.has_full_disk_read_access() {
|
||||
if apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access() {
|
||||
if !file_system_sandbox_policy.has_full_disk_read_access() {
|
||||
return Err(CodexErr::UnsupportedOperation(
|
||||
"Restricted read-only access is not supported by the legacy Linux Landlock filesystem backend."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let writable_roots = sandbox_policy
|
||||
let writable_roots = file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(|writable_root| writable_root.root)
|
||||
|
||||
@@ -10,14 +10,14 @@ use std::path::PathBuf;
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::landlock::apply_sandbox_policy_to_current_thread;
|
||||
use crate::landlock::apply_permission_profile_to_current_thread;
|
||||
use crate::launcher::exec_bwrap;
|
||||
use crate::launcher::preferred_bwrap_supports_argv0;
|
||||
use crate::proxy_routing::activate_proxy_routes_in_netns;
|
||||
use crate::proxy_routing::prepare_host_proxy_route_spec;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -41,18 +41,13 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "command-cwd", hide = true)]
|
||||
pub command_cwd: Option<PathBuf>,
|
||||
|
||||
/// Legacy compatibility policy.
|
||||
///
|
||||
/// Newer callers pass split filesystem/network policies as well so the
|
||||
/// helper can migrate incrementally without breaking older invocations.
|
||||
#[arg(long = "sandbox-policy", hide = true)]
|
||||
pub sandbox_policy: Option<SandboxPolicy>,
|
||||
|
||||
#[arg(long = "file-system-sandbox-policy", hide = true)]
|
||||
pub file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
|
||||
|
||||
#[arg(long = "network-sandbox-policy", hide = true)]
|
||||
pub network_sandbox_policy: Option<NetworkSandboxPolicy>,
|
||||
/// Canonical runtime permissions for the command.
|
||||
#[arg(
|
||||
long = "permission-profile",
|
||||
hide = true,
|
||||
value_parser = parse_permission_profile
|
||||
)]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
|
||||
/// Opt-in: use the legacy Landlock Linux sandbox fallback.
|
||||
///
|
||||
@@ -102,9 +97,7 @@ pub fn run_main() -> ! {
|
||||
let LandlockCommand {
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
permission_profile,
|
||||
use_legacy_landlock,
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
@@ -117,17 +110,11 @@ pub fn run_main() -> ! {
|
||||
panic!("No command specified to execute.");
|
||||
}
|
||||
ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec, use_legacy_landlock);
|
||||
let EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
let EffectivePermissions {
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
} = resolve_sandbox_policies(
|
||||
sandbox_policy_cwd.as_path(),
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
.unwrap_or_else(|err| panic!("{err}"));
|
||||
} = resolve_permission_profile(permission_profile).unwrap_or_else(|err| panic!("{err}"));
|
||||
ensure_legacy_landlock_mode_supports_policy(
|
||||
use_legacy_landlock,
|
||||
&file_system_sandbox_policy,
|
||||
@@ -147,9 +134,8 @@ pub fn run_main() -> ! {
|
||||
}
|
||||
}
|
||||
let proxy_routing_active = allow_network_for_proxy;
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
/*apply_landlock_fs*/ false,
|
||||
allow_network_for_proxy,
|
||||
@@ -161,9 +147,8 @@ pub fn run_main() -> ! {
|
||||
}
|
||||
|
||||
if file_system_sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
/*apply_landlock_fs*/ false,
|
||||
allow_network_for_proxy,
|
||||
@@ -189,9 +174,7 @@ pub fn run_main() -> ! {
|
||||
let inner = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: &sandbox_policy_cwd,
|
||||
command_cwd: command_cwd.as_deref(),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
command,
|
||||
@@ -208,9 +191,8 @@ pub fn run_main() -> ! {
|
||||
}
|
||||
|
||||
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
&permission_profile,
|
||||
&sandbox_policy_cwd,
|
||||
/*apply_landlock_fs*/ true,
|
||||
allow_network_for_proxy,
|
||||
@@ -222,164 +204,41 @@ pub fn run_main() -> ! {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EffectiveSandboxPolicies {
|
||||
sandbox_policy: SandboxPolicy,
|
||||
struct EffectivePermissions {
|
||||
permission_profile: PermissionProfile,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum ResolveSandboxPoliciesError {
|
||||
PartialSplitPolicies,
|
||||
SplitPoliciesRequireDirectRuntimeEnforcement(String),
|
||||
FailedToDeriveLegacyPolicy(String),
|
||||
MismatchedLegacyPolicy {
|
||||
provided: SandboxPolicy,
|
||||
derived: SandboxPolicy,
|
||||
},
|
||||
enum ResolvePermissionProfileError {
|
||||
MissingConfiguration,
|
||||
}
|
||||
|
||||
impl fmt::Display for ResolveSandboxPoliciesError {
|
||||
impl fmt::Display for ResolvePermissionProfileError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::PartialSplitPolicies => {
|
||||
write!(
|
||||
f,
|
||||
"file-system and network sandbox policies must be provided together"
|
||||
)
|
||||
}
|
||||
Self::SplitPoliciesRequireDirectRuntimeEnforcement(err) => {
|
||||
write!(
|
||||
f,
|
||||
"split sandbox policies require direct runtime enforcement and cannot be paired with legacy sandbox policy: {err}"
|
||||
)
|
||||
}
|
||||
Self::FailedToDeriveLegacyPolicy(err) => {
|
||||
write!(
|
||||
f,
|
||||
"failed to derive legacy sandbox policy from split policies: {err}"
|
||||
)
|
||||
}
|
||||
Self::MismatchedLegacyPolicy { provided, derived } => {
|
||||
write!(
|
||||
f,
|
||||
"legacy sandbox policy must match split sandbox policies: provided={provided:?}, derived={derived:?}"
|
||||
)
|
||||
}
|
||||
Self::MissingConfiguration => write!(f, "missing sandbox policy configuration"),
|
||||
Self::MissingConfiguration => write!(f, "missing permission profile configuration"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_sandbox_policies(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
file_system_sandbox_policy: Option<FileSystemSandboxPolicy>,
|
||||
network_sandbox_policy: Option<NetworkSandboxPolicy>,
|
||||
) -> Result<EffectiveSandboxPolicies, ResolveSandboxPoliciesError> {
|
||||
// Accept either a fully legacy policy, a fully split policy pair, or all
|
||||
// three views together. Reject partial split-policy input so the helper
|
||||
// never runs with mismatched filesystem/network state.
|
||||
let split_policies = match (file_system_sandbox_policy, network_sandbox_policy) {
|
||||
(Some(file_system_sandbox_policy), Some(network_sandbox_policy)) => {
|
||||
Some((file_system_sandbox_policy, network_sandbox_policy))
|
||||
}
|
||||
(None, None) => None,
|
||||
_ => return Err(ResolveSandboxPoliciesError::PartialSplitPolicies),
|
||||
};
|
||||
|
||||
match (sandbox_policy, split_policies) {
|
||||
(Some(sandbox_policy), Some((file_system_sandbox_policy, network_sandbox_policy))) => {
|
||||
if file_system_sandbox_policy
|
||||
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd)
|
||||
{
|
||||
return Ok(EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
});
|
||||
}
|
||||
let derived_legacy_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd)
|
||||
.map_err(|err| {
|
||||
ResolveSandboxPoliciesError::SplitPoliciesRequireDirectRuntimeEnforcement(
|
||||
err.to_string(),
|
||||
)
|
||||
})?;
|
||||
if !legacy_sandbox_policies_match_semantics(
|
||||
&sandbox_policy,
|
||||
&derived_legacy_policy,
|
||||
sandbox_policy_cwd,
|
||||
) {
|
||||
return Err(ResolveSandboxPoliciesError::MismatchedLegacyPolicy {
|
||||
provided: sandbox_policy,
|
||||
derived: derived_legacy_policy,
|
||||
});
|
||||
}
|
||||
Ok(EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
})
|
||||
}
|
||||
(Some(sandbox_policy), None) => Ok(EffectiveSandboxPolicies {
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
&sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
sandbox_policy,
|
||||
}),
|
||||
(None, Some((file_system_sandbox_policy, network_sandbox_policy))) => {
|
||||
let sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, sandbox_policy_cwd)
|
||||
.map_err(|err| {
|
||||
ResolveSandboxPoliciesError::FailedToDeriveLegacyPolicy(err.to_string())
|
||||
})?;
|
||||
Ok(EffectiveSandboxPolicies {
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
})
|
||||
}
|
||||
(None, None) => Err(ResolveSandboxPoliciesError::MissingConfiguration),
|
||||
}
|
||||
fn parse_permission_profile(value: &str) -> std::result::Result<PermissionProfile, String> {
|
||||
serde_json::from_str(value).map_err(|err| format!("invalid permission profile JSON: {err}"))
|
||||
}
|
||||
|
||||
fn legacy_sandbox_policies_match_semantics(
|
||||
provided: &SandboxPolicy,
|
||||
derived: &SandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
) -> bool {
|
||||
NetworkSandboxPolicy::from(provided) == NetworkSandboxPolicy::from(derived)
|
||||
&& file_system_sandbox_policies_match_semantics(
|
||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
provided,
|
||||
sandbox_policy_cwd,
|
||||
),
|
||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
derived,
|
||||
sandbox_policy_cwd,
|
||||
),
|
||||
sandbox_policy_cwd,
|
||||
)
|
||||
}
|
||||
|
||||
fn file_system_sandbox_policies_match_semantics(
|
||||
provided: &FileSystemSandboxPolicy,
|
||||
derived: &FileSystemSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
) -> bool {
|
||||
provided.has_full_disk_read_access() == derived.has_full_disk_read_access()
|
||||
&& provided.has_full_disk_write_access() == derived.has_full_disk_write_access()
|
||||
&& provided.include_platform_defaults() == derived.include_platform_defaults()
|
||||
&& provided.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||
== derived.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||
&& provided.get_writable_roots_with_cwd(sandbox_policy_cwd)
|
||||
== derived.get_writable_roots_with_cwd(sandbox_policy_cwd)
|
||||
&& provided.get_unreadable_roots_with_cwd(sandbox_policy_cwd)
|
||||
== derived.get_unreadable_roots_with_cwd(sandbox_policy_cwd)
|
||||
fn resolve_permission_profile(
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
) -> Result<EffectivePermissions, ResolvePermissionProfileError> {
|
||||
let permission_profile =
|
||||
permission_profile.ok_or(ResolvePermissionProfileError::MissingConfiguration)?;
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
Ok(EffectivePermissions {
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_legacy_landlock: bool) {
|
||||
@@ -399,7 +258,7 @@ fn ensure_legacy_landlock_mode_supports_policy(
|
||||
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd)
|
||||
{
|
||||
panic!(
|
||||
"split sandbox policies requiring direct runtime enforcement are incompatible with --use-legacy-landlock"
|
||||
"permission profiles requiring direct runtime enforcement are incompatible with --use-legacy-landlock"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -656,9 +515,7 @@ fn is_proc_mount_failure(stderr: &str) -> bool {
|
||||
struct InnerSeccompCommandArgs<'a> {
|
||||
sandbox_policy_cwd: &'a Path,
|
||||
command_cwd: Option<&'a Path>,
|
||||
sandbox_policy: &'a SandboxPolicy,
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
permission_profile: &'a PermissionProfile,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_route_spec: Option<String>,
|
||||
command: Vec<String>,
|
||||
@@ -669,9 +526,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
let InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd,
|
||||
command_cwd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
permission_profile,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
command,
|
||||
@@ -680,17 +535,9 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
Ok(path) => path,
|
||||
Err(err) => panic!("failed to resolve current executable path: {err}"),
|
||||
};
|
||||
let policy_json = match serde_json::to_string(sandbox_policy) {
|
||||
let permission_profile_json = match serde_json::to_string(permission_profile) {
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize sandbox policy: {err}"),
|
||||
};
|
||||
let file_system_policy_json = match serde_json::to_string(file_system_sandbox_policy) {
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize filesystem sandbox policy: {err}"),
|
||||
};
|
||||
let network_policy_json = match serde_json::to_string(&network_sandbox_policy) {
|
||||
Ok(json) => json,
|
||||
Err(err) => panic!("failed to serialize network sandbox policy: {err}"),
|
||||
Err(err) => panic!("failed to serialize permission profile: {err}"),
|
||||
};
|
||||
|
||||
let mut inner = vec![
|
||||
@@ -703,12 +550,8 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
inner.push(command_cwd.to_string_lossy().to_string());
|
||||
}
|
||||
inner.extend([
|
||||
"--sandbox-policy".to_string(),
|
||||
policy_json,
|
||||
"--file-system-sandbox-policy".to_string(),
|
||||
file_system_policy_json,
|
||||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
"--permission-profile".to_string(),
|
||||
permission_profile_json,
|
||||
"--apply-seccomp-then-exec".to_string(),
|
||||
]);
|
||||
if allow_network_for_proxy {
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
#[cfg(test)]
|
||||
use super::*;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::NetworkSandboxPolicy;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
#[cfg(test)]
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
#[cfg(test)]
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn read_only_permission_profile() -> PermissionProfile {
|
||||
PermissionProfile::read_only()
|
||||
}
|
||||
|
||||
fn read_only_file_system_policy() -> FileSystemSandboxPolicy {
|
||||
read_only_permission_profile().file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_proc_mount_invalid_argument_failure() {
|
||||
let stderr = "bwrap: Can't mount proc on /newroot/proc: Invalid argument";
|
||||
@@ -37,10 +45,10 @@ fn ignores_non_proc_mount_errors() {
|
||||
|
||||
#[test]
|
||||
fn inserts_bwrap_argv0_before_command_separator() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let mut argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
@@ -80,10 +88,10 @@ fn inserts_bwrap_argv0_before_command_separator() {
|
||||
|
||||
#[test]
|
||||
fn rewrites_inner_command_path_when_bwrap_lacks_argv0() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let mut argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
@@ -148,10 +156,10 @@ fn rewrites_bwrap_helper_command_not_nested_user_command_when_current_exe_appear
|
||||
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
@@ -166,10 +174,10 @@ fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = read_only_file_system_policy();
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
@@ -250,7 +258,7 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
|
||||
let argv = build_preflight_bwrap_argv(
|
||||
Path::new("/"),
|
||||
Path::new("/"),
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
mode,
|
||||
)
|
||||
.args;
|
||||
@@ -259,13 +267,11 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() {
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_inner_command_includes_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let permission_profile = read_only_permission_profile();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: Some("{\"routes\":[]}".to_string()),
|
||||
command: vec!["/bin/true".to_string()],
|
||||
@@ -276,21 +282,18 @@ fn managed_proxy_inner_command_includes_route_spec() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_command_includes_split_policy_flags() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
fn inner_command_includes_permission_profile_flag() {
|
||||
let permission_profile = read_only_permission_profile();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: false,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
|
||||
assert!(args.iter().any(|arg| arg == "--file-system-sandbox-policy"));
|
||||
assert!(args.iter().any(|arg| arg == "--network-sandbox-policy"));
|
||||
assert!(args.iter().any(|arg| arg == "--permission-profile"));
|
||||
assert!(
|
||||
args.windows(2)
|
||||
.any(|window| { window == ["--command-cwd", "/tmp/link"] })
|
||||
@@ -299,13 +302,11 @@ fn inner_command_includes_split_policy_flags() {
|
||||
|
||||
#[test]
|
||||
fn non_managed_inner_command_omits_route_spec() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let permission_profile = read_only_permission_profile();
|
||||
let args = build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: false,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
@@ -317,13 +318,11 @@ fn non_managed_inner_command_omits_route_spec() {
|
||||
#[test]
|
||||
fn managed_proxy_inner_command_requires_route_spec() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let permission_profile = read_only_permission_profile();
|
||||
build_inner_seccomp_command(InnerSeccompCommandArgs {
|
||||
sandbox_policy_cwd: Path::new("/tmp"),
|
||||
command_cwd: Some(Path::new("/tmp/link")),
|
||||
sandbox_policy: &sandbox_policy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
@@ -333,89 +332,28 @@ fn managed_proxy_inner_command_requires_route_spec() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_derives_split_policies_from_legacy_policy() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
fn resolve_permission_profile_derives_runtime_policies() {
|
||||
let permission_profile = read_only_permission_profile();
|
||||
let resolved = resolve_permission_profile(Some(permission_profile.clone()))
|
||||
.expect("profile should resolve");
|
||||
|
||||
let resolved = resolve_sandbox_policies(
|
||||
Path::new("/tmp"),
|
||||
Some(sandbox_policy.clone()),
|
||||
/*file_system_sandbox_policy*/ None,
|
||||
/*network_sandbox_policy*/ None,
|
||||
)
|
||||
.expect("legacy policy should resolve");
|
||||
|
||||
assert_eq!(resolved.sandbox_policy, sandbox_policy);
|
||||
assert_eq!(resolved.permission_profile, permission_profile);
|
||||
assert_eq!(
|
||||
resolved.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::from(&sandbox_policy)
|
||||
read_only_file_system_policy()
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.network_sandbox_policy,
|
||||
NetworkSandboxPolicy::from(&sandbox_policy)
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_derives_legacy_policy_from_split_policies() {
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
|
||||
let resolved = resolve_sandbox_policies(
|
||||
Path::new("/tmp"),
|
||||
/*sandbox_policy*/ None,
|
||||
Some(file_system_sandbox_policy.clone()),
|
||||
Some(network_sandbox_policy),
|
||||
)
|
||||
.expect("split policies should resolve");
|
||||
|
||||
assert_eq!(resolved.sandbox_policy, sandbox_policy);
|
||||
assert_eq!(
|
||||
resolved.file_system_sandbox_policy,
|
||||
file_system_sandbox_policy
|
||||
);
|
||||
assert_eq!(resolved.network_sandbox_policy, network_sandbox_policy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_rejects_partial_split_policies() {
|
||||
let err = resolve_sandbox_policies(
|
||||
Path::new("/tmp"),
|
||||
Some(SandboxPolicy::new_read_only_policy()),
|
||||
Some(FileSystemSandboxPolicy::default()),
|
||||
/*network_sandbox_policy*/ None,
|
||||
)
|
||||
.expect_err("partial split policies should fail");
|
||||
|
||||
assert_eq!(err, ResolveSandboxPoliciesError::PartialSplitPolicies);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_rejects_mismatched_legacy_and_split_inputs() {
|
||||
let err = resolve_sandbox_policies(
|
||||
Path::new("/tmp"),
|
||||
Some(SandboxPolicy::new_read_only_policy()),
|
||||
Some(FileSystemSandboxPolicy::unrestricted()),
|
||||
Some(NetworkSandboxPolicy::Enabled),
|
||||
)
|
||||
.expect_err("mismatched legacy and split policies should fail");
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
ResolveSandboxPoliciesError::MismatchedLegacyPolicy { .. }
|
||||
),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_accepts_split_policies_requiring_direct_runtime_enforcement() {
|
||||
fn resolve_permission_profile_preserves_direct_runtime_profile() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let docs = temp_dir.path().join("docs");
|
||||
std::fs::create_dir_all(&docs).expect("create docs");
|
||||
let docs = AbsolutePathBuf::from_absolute_path(&docs).expect("absolute docs");
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
codex_protocol::permissions::FileSystemSandboxEntry {
|
||||
path: codex_protocol::permissions::FileSystemPath::Special {
|
||||
@@ -428,16 +366,14 @@ fn resolve_sandbox_policies_accepts_split_policies_requiring_direct_runtime_enfo
|
||||
access: codex_protocol::permissions::FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
let resolved = resolve_permission_profile(Some(permission_profile.clone()))
|
||||
.expect("profile should resolve");
|
||||
|
||||
let resolved = resolve_sandbox_policies(
|
||||
temp_dir.path(),
|
||||
Some(sandbox_policy.clone()),
|
||||
Some(file_system_sandbox_policy.clone()),
|
||||
Some(NetworkSandboxPolicy::Restricted),
|
||||
)
|
||||
.expect("split-only policy should preserve provided legacy fallback");
|
||||
|
||||
assert_eq!(resolved.sandbox_policy, sandbox_policy);
|
||||
assert_eq!(resolved.permission_profile, permission_profile);
|
||||
assert_eq!(
|
||||
resolved.file_system_sandbox_policy,
|
||||
file_system_sandbox_policy
|
||||
@@ -449,37 +385,10 @@ fn resolve_sandbox_policies_accepts_split_policies_requiring_direct_runtime_enfo
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_sandbox_policies_accepts_semantically_equivalent_workspace_write_inputs() {
|
||||
let temp_dir = tempfile::TempDir::new().expect("tempdir");
|
||||
let workspace = temp_dir.path().join("workspace");
|
||||
std::fs::create_dir_all(&workspace).expect("create workspace");
|
||||
let workspace = AbsolutePathBuf::from_absolute_path(&workspace).expect("absolute workspace");
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![workspace],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from(&SandboxPolicy::new_workspace_write_policy());
|
||||
fn resolve_permission_profile_rejects_missing_configuration() {
|
||||
let err = resolve_permission_profile(None).expect_err("missing profile should fail");
|
||||
|
||||
let resolved = resolve_sandbox_policies(
|
||||
temp_dir.path().join("workspace").as_path(),
|
||||
Some(sandbox_policy.clone()),
|
||||
Some(file_system_sandbox_policy.clone()),
|
||||
Some(NetworkSandboxPolicy::Restricted),
|
||||
)
|
||||
.expect("semantically equivalent legacy workspace-write policy should resolve");
|
||||
|
||||
assert_eq!(resolved.sandbox_policy, sandbox_policy);
|
||||
assert_eq!(
|
||||
resolved.file_system_sandbox_policy,
|
||||
file_system_sandbox_policy
|
||||
);
|
||||
assert_eq!(
|
||||
resolved.network_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
assert_eq!(err, ResolvePermissionProfileError::MissingConfiguration);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,14 +11,12 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
@@ -83,37 +81,32 @@ async fn run_cmd_result_with_writable_roots(
|
||||
use_legacy_landlock: bool,
|
||||
network_access: bool,
|
||||
) -> Result<codex_protocol::exec_output::ExecToolCallOutput> {
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: writable_roots
|
||||
.iter()
|
||||
.map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap())
|
||||
.collect(),
|
||||
network_access,
|
||||
let writable_roots = writable_roots
|
||||
.iter()
|
||||
.map(|path| AbsolutePathBuf::try_from(path.as_path()).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let permission_profile = PermissionProfile::workspace_write_with(
|
||||
&writable_roots,
|
||||
if network_access {
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
NetworkSandboxPolicy::Restricted
|
||||
},
|
||||
// Exclude tmp-related folders from writable roots because we need a
|
||||
// folder that is writable by tests but that we intentionally disallow
|
||||
// writing to in the sandbox.
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
run_cmd_result_with_policies(
|
||||
cmd,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
timeout_ms,
|
||||
use_legacy_landlock,
|
||||
)
|
||||
.await
|
||||
/*exclude_tmpdir_env_var*/
|
||||
true,
|
||||
/*exclude_slash_tmp*/ true,
|
||||
);
|
||||
run_cmd_result_with_permission_profile(cmd, permission_profile, timeout_ms, use_legacy_landlock)
|
||||
.await
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
async fn run_cmd_result_with_policies(
|
||||
async fn run_cmd_result_with_permission_profile(
|
||||
cmd: &[&str],
|
||||
sandbox_policy: SandboxPolicy,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
permission_profile: PermissionProfile,
|
||||
timeout_ms: u64,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Result<codex_protocol::exec_output::ExecToolCallOutput> {
|
||||
@@ -134,11 +127,6 @@ async fn run_cmd_result_with_policies(
|
||||
};
|
||||
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
|
||||
let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program));
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
|
||||
process_exec_tool_call(
|
||||
params,
|
||||
@@ -396,10 +384,9 @@ async fn assert_network_blocked(cmd: &[&str]) {
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox");
|
||||
let codex_linux_sandbox_exe: Option<PathBuf> = Some(PathBuf::from(sandbox_program));
|
||||
let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy);
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
let result = process_exec_tool_call(
|
||||
params,
|
||||
&permission_profile,
|
||||
@@ -561,12 +548,6 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() {
|
||||
.expect("sandbox helper should have a parent")
|
||||
.to_path_buf();
|
||||
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir")],
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -594,16 +575,18 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() {
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
let output = expect_denied(
|
||||
run_cmd_result_with_policies(
|
||||
run_cmd_result_with_permission_profile(
|
||||
&[
|
||||
"bash",
|
||||
"-lc",
|
||||
&format!("echo denied > {}", blocked_target.to_string_lossy()),
|
||||
],
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
permission_profile,
|
||||
LONG_TIMEOUT_MS,
|
||||
/*use_legacy_landlock*/ false,
|
||||
)
|
||||
@@ -633,12 +616,6 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() {
|
||||
.expect("sandbox helper should have a parent")
|
||||
.to_path_buf();
|
||||
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir")],
|
||||
network_access: true,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -672,7 +649,11 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() {
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
let output = run_cmd_result_with_policies(
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
let output = run_cmd_result_with_permission_profile(
|
||||
&[
|
||||
"bash",
|
||||
"-lc",
|
||||
@@ -682,9 +663,7 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() {
|
||||
allowed_target.to_string_lossy()
|
||||
),
|
||||
],
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
permission_profile,
|
||||
LONG_TIMEOUT_MS,
|
||||
/*use_legacy_landlock*/ false,
|
||||
)
|
||||
@@ -708,9 +687,6 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() {
|
||||
let blocked_target = blocked.join("secret.txt");
|
||||
std::fs::write(&blocked_target, "secret").expect("seed blocked file");
|
||||
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
network_access: true,
|
||||
};
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -725,16 +701,18 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() {
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
let output = expect_denied(
|
||||
run_cmd_result_with_policies(
|
||||
run_cmd_result_with_permission_profile(
|
||||
&[
|
||||
"bash",
|
||||
"-lc",
|
||||
&format!("cat {}", blocked_target.to_string_lossy()),
|
||||
],
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
permission_profile,
|
||||
LONG_TIMEOUT_MS,
|
||||
/*use_legacy_landlock*/ false,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
@@ -65,7 +65,7 @@ async fn should_skip_bwrap_tests() -> bool {
|
||||
|
||||
let output = run_linux_sandbox_direct(
|
||||
&["bash", "-c", "true"],
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
&PermissionProfile::read_only(),
|
||||
/*allow_network_for_proxy*/ false,
|
||||
env,
|
||||
NETWORK_TIMEOUT_MS,
|
||||
@@ -91,7 +91,7 @@ async fn managed_proxy_skip_reason() -> Option<String> {
|
||||
|
||||
let output = run_linux_sandbox_direct(
|
||||
&["bash", "-c", "true"],
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&PermissionProfile::Disabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
env,
|
||||
NETWORK_TIMEOUT_MS,
|
||||
@@ -114,7 +114,7 @@ async fn managed_proxy_skip_reason() -> Option<String> {
|
||||
|
||||
async fn run_linux_sandbox_direct(
|
||||
command: &[&str],
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
allow_network_for_proxy: bool,
|
||||
env: HashMap<String, String>,
|
||||
timeout_ms: u64,
|
||||
@@ -123,16 +123,16 @@ async fn run_linux_sandbox_direct(
|
||||
Ok(cwd) => cwd,
|
||||
Err(err) => panic!("cwd should exist: {err}"),
|
||||
};
|
||||
let policy_json = match serde_json::to_string(sandbox_policy) {
|
||||
Ok(policy_json) => policy_json,
|
||||
Err(err) => panic!("policy should serialize: {err}"),
|
||||
let permission_profile_json = match serde_json::to_string(permission_profile) {
|
||||
Ok(permission_profile_json) => permission_profile_json,
|
||||
Err(err) => panic!("permission profile should serialize: {err}"),
|
||||
};
|
||||
|
||||
let mut args = vec![
|
||||
"--sandbox-policy-cwd".to_string(),
|
||||
cwd.to_string_lossy().to_string(),
|
||||
"--sandbox-policy".to_string(),
|
||||
policy_json,
|
||||
"--permission-profile".to_string(),
|
||||
permission_profile_json,
|
||||
];
|
||||
if allow_network_for_proxy {
|
||||
args.push("--allow-network-for-proxy".to_string());
|
||||
@@ -170,7 +170,7 @@ async fn managed_proxy_mode_fails_closed_without_proxy_env() {
|
||||
|
||||
let output = run_linux_sandbox_direct(
|
||||
&["bash", "-c", "true"],
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&PermissionProfile::Disabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
env,
|
||||
NETWORK_TIMEOUT_MS,
|
||||
@@ -225,7 +225,7 @@ async fn managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress() {
|
||||
"-c",
|
||||
"proxy=\"${HTTP_PROXY#*://}\"; host=\"${proxy%%:*}\"; port=\"${proxy##*:}\"; exec 3<>/dev/tcp/${host}/${port}; printf 'GET http://example.com/ HTTP/1.1\\r\\nHost: example.com\\r\\n\\r\\n' >&3; IFS= read -r line <&3; printf '%s\\n' \"$line\"",
|
||||
],
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&PermissionProfile::Disabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
env.clone(),
|
||||
NETWORK_TIMEOUT_MS,
|
||||
@@ -256,7 +256,7 @@ async fn managed_proxy_mode_routes_through_bridge_and_blocks_direct_egress() {
|
||||
|
||||
let direct_egress_output = run_linux_sandbox_direct(
|
||||
&["bash", "-c", "echo hi > /dev/tcp/192.0.2.1/80"],
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&PermissionProfile::Disabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
env,
|
||||
NETWORK_TIMEOUT_MS,
|
||||
@@ -294,7 +294,7 @@ async fn managed_proxy_mode_denies_af_unix_creation_for_user_command() {
|
||||
"-c",
|
||||
"import socket,sys\ntry:\n socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\nexcept PermissionError:\n sys.exit(0)\nexcept OSError:\n sys.exit(2)\nsys.exit(1)\n",
|
||||
],
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&PermissionProfile::Disabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
env,
|
||||
NETWORK_TIMEOUT_MS,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use std::path::Path;
|
||||
|
||||
/// Basename used when the Codex executable self-invokes as the Linux sandbox
|
||||
@@ -14,30 +12,24 @@ pub fn allow_network_for_proxy(enforce_managed_network: bool) -> bool {
|
||||
enforce_managed_network
|
||||
}
|
||||
|
||||
/// Converts the sandbox policies into the CLI invocation for
|
||||
/// Converts the permission profile into the CLI invocation for
|
||||
/// `codex-linux-sandbox`.
|
||||
///
|
||||
/// The helper performs the actual sandboxing (bubblewrap by default + seccomp) after
|
||||
/// parsing these arguments. Policy JSON flags are emitted before helper feature
|
||||
/// flags so the argv order matches the helper's CLI shape. See
|
||||
/// The helper performs the actual sandboxing (bubblewrap by default + seccomp)
|
||||
/// after parsing these arguments. The profile JSON flag is emitted before
|
||||
/// helper feature flags so the argv order matches the helper's CLI shape. See
|
||||
/// `docs/linux_sandbox.md` for the Linux semantics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_linux_sandbox_command_args_for_policies(
|
||||
pub fn create_linux_sandbox_command_args_for_permission_profile(
|
||||
command: Vec<String>,
|
||||
command_cwd: &Path,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_policy_cwd: &Path,
|
||||
use_legacy_landlock: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> Vec<String> {
|
||||
let sandbox_policy_json = serde_json::to_string(sandbox_policy)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize sandbox policy: {err}"));
|
||||
let file_system_policy_json = serde_json::to_string(file_system_sandbox_policy)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize filesystem sandbox policy: {err}"));
|
||||
let network_policy_json = serde_json::to_string(&network_sandbox_policy)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize network sandbox policy: {err}"));
|
||||
let permission_profile_json = serde_json::to_string(permission_profile)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize permission profile: {err}"));
|
||||
let sandbox_policy_cwd = sandbox_policy_cwd
|
||||
.to_str()
|
||||
.unwrap_or_else(|| panic!("cwd must be valid UTF-8"))
|
||||
@@ -52,12 +44,8 @@ pub fn create_linux_sandbox_command_args_for_policies(
|
||||
sandbox_policy_cwd,
|
||||
"--command-cwd".to_string(),
|
||||
command_cwd,
|
||||
"--sandbox-policy".to_string(),
|
||||
sandbox_policy_json,
|
||||
"--file-system-sandbox-policy".to_string(),
|
||||
file_system_policy_json,
|
||||
"--network-sandbox-policy".to_string(),
|
||||
network_policy_json,
|
||||
"--permission-profile".to_string(),
|
||||
permission_profile_json,
|
||||
];
|
||||
if use_legacy_landlock {
|
||||
linux_cmd.push("--use-legacy-landlock".to_string());
|
||||
|
||||
@@ -52,20 +52,16 @@ fn proxy_flag_is_included_when_requested() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_policy_flags_are_included() {
|
||||
fn permission_profile_flag_is_included() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let command_cwd = Path::new("/tmp/link");
|
||||
let cwd = Path::new("/tmp");
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from(&sandbox_policy);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
let permission_profile = PermissionProfile::read_only();
|
||||
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
let args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
command,
|
||||
command_cwd,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
&permission_profile,
|
||||
cwd,
|
||||
/*use_legacy_landlock*/ true,
|
||||
/*allow_network_for_proxy*/ false,
|
||||
@@ -73,12 +69,7 @@ fn split_policy_flags_are_included() {
|
||||
|
||||
assert_eq!(
|
||||
args.windows(2)
|
||||
.any(|window| { window[0] == "--file-system-sandbox-policy" && !window[1].is_empty() }),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
args.windows(2)
|
||||
.any(|window| window[0] == "--network-sandbox-policy" && window[1] == "\"restricted\""),
|
||||
.any(|window| { window[0] == "--permission-profile" && !window[1].is_empty() }),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::bwrap::WSL1_BWRAP_WARNING;
|
||||
use crate::bwrap::is_wsl1;
|
||||
use crate::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use crate::landlock::allow_network_for_proxy;
|
||||
use crate::landlock::create_linux_sandbox_command_args_for_policies;
|
||||
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||
use crate::policy_transforms::effective_permission_profile;
|
||||
use crate::policy_transforms::should_require_platform_sandbox;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
@@ -186,12 +186,6 @@ impl SandboxManager {
|
||||
effective_permission_profile(permissions, additional_permissions.as_ref());
|
||||
let (effective_file_system_policy, effective_network_policy) =
|
||||
effective_permission_profile.to_runtime_permissions();
|
||||
let effective_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&effective_permission_profile,
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
sandbox_policy_cwd,
|
||||
);
|
||||
let mut argv = Vec::with_capacity(1 + command.args.len());
|
||||
argv.push(command.program);
|
||||
argv.extend(command.args.into_iter().map(OsString::from));
|
||||
@@ -231,12 +225,10 @@ impl SandboxManager {
|
||||
allow_proxy_network,
|
||||
is_wsl1(),
|
||||
)?;
|
||||
let mut args = create_linux_sandbox_command_args_for_policies(
|
||||
let mut args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
os_argv_to_strings(argv),
|
||||
command.cwd.as_path(),
|
||||
&effective_policy,
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
&effective_permission_profile,
|
||||
sandbox_policy_cwd,
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
|
||||
@@ -140,6 +140,8 @@ pub use tool_spec::create_local_shell_tool;
|
||||
pub use tool_spec::create_tools_json_for_responses_api;
|
||||
pub use tool_spec::create_web_search_tool;
|
||||
pub use tool_suggest::TOOL_SUGGEST_APPROVAL_KIND_VALUE;
|
||||
pub use tool_suggest::TOOL_SUGGEST_PERSIST_ALWAYS_VALUE;
|
||||
pub use tool_suggest::TOOL_SUGGEST_PERSIST_KEY;
|
||||
pub use tool_suggest::ToolSuggestArgs;
|
||||
pub use tool_suggest::ToolSuggestMeta;
|
||||
pub use tool_suggest::ToolSuggestResult;
|
||||
|
||||
@@ -14,6 +14,8 @@ use crate::DiscoverableToolAction;
|
||||
use crate::DiscoverableToolType;
|
||||
|
||||
pub const TOOL_SUGGEST_APPROVAL_KIND_VALUE: &str = "tool_suggestion";
|
||||
pub const TOOL_SUGGEST_PERSIST_KEY: &str = "persist";
|
||||
pub const TOOL_SUGGEST_PERSIST_ALWAYS_VALUE: &str = "always";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ToolSuggestArgs {
|
||||
@@ -37,6 +39,7 @@ pub struct ToolSuggestResult {
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct ToolSuggestMeta<'a> {
|
||||
pub codex_approval_kind: &'static str,
|
||||
pub persist: &'static str,
|
||||
pub tool_type: DiscoverableToolType,
|
||||
pub suggest_type: DiscoverableToolAction,
|
||||
pub suggest_reason: &'a str,
|
||||
@@ -111,6 +114,7 @@ fn build_tool_suggestion_meta<'a>(
|
||||
) -> ToolSuggestMeta<'a> {
|
||||
ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
persist: TOOL_SUGGEST_PERSIST_ALWAYS_VALUE,
|
||||
tool_type,
|
||||
suggest_type: action_type,
|
||||
suggest_reason,
|
||||
|
||||
@@ -48,6 +48,7 @@ fn build_tool_suggestion_elicitation_request_uses_expected_shape() {
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: Some(json!(ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
persist: TOOL_SUGGEST_PERSIST_ALWAYS_VALUE,
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
suggest_type: DiscoverableToolAction::Install,
|
||||
suggest_reason: "Plan and reference events from your calendar",
|
||||
@@ -104,6 +105,7 @@ fn build_tool_suggestion_elicitation_request_for_plugin_omits_install_url() {
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: Some(json!(ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
persist: TOOL_SUGGEST_PERSIST_ALWAYS_VALUE,
|
||||
tool_type: DiscoverableToolType::Plugin,
|
||||
suggest_type: DiscoverableToolAction::Install,
|
||||
suggest_reason: "Use the sample plugin's skills and MCP server",
|
||||
@@ -138,6 +140,7 @@ fn build_tool_suggestion_meta_uses_expected_shape() {
|
||||
meta,
|
||||
ToolSuggestMeta {
|
||||
codex_approval_kind: TOOL_SUGGEST_APPROVAL_KIND_VALUE,
|
||||
persist: TOOL_SUGGEST_PERSIST_ALWAYS_VALUE,
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
suggest_type: DiscoverableToolAction::Install,
|
||||
suggest_reason: "Find and reference emails from your inbox",
|
||||
|
||||
@@ -48,6 +48,16 @@ Use `$` in the composer to insert a ChatGPT connector; the popover lists accessi
|
||||
apps. The `/apps` command lists available and installed apps. Connected apps appear first
|
||||
and are labeled as connected; others are marked as can be installed.
|
||||
|
||||
Codex stores "never show again" choices for tool suggestions in `config.toml`:
|
||||
|
||||
```toml
|
||||
[tool_suggest]
|
||||
disabled_tools = [
|
||||
{ type = "plugin", id = "slack@openai-curated" },
|
||||
{ type = "connector", id = "connector_google_calendar" },
|
||||
]
|
||||
```
|
||||
|
||||
## Notify
|
||||
|
||||
Codex can run a notification hook when the agent finishes a turn. See the configuration reference for the latest notification settings:
|
||||
|
||||
Reference in New Issue
Block a user