mirror of
https://github.com/openai/codex.git
synced 2026-03-13 02:03:59 +00:00
Compare commits
3 Commits
bot/update
...
dev/cc/mul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d511cfe6d | ||
|
|
19236d6e51 | ||
|
|
a5c6467cf9 |
@@ -171,6 +171,8 @@ use crate::error::CodexErr;
|
||||
use crate::error::Result as CodexResult;
|
||||
#[cfg(test)]
|
||||
use crate::exec::StreamOutput;
|
||||
use crate::network_proxy_registry::NetworkProxyRegistry;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
|
||||
mod rollout_reconstruction;
|
||||
@@ -275,6 +277,7 @@ use crate::skills::collect_explicit_skill_mentions;
|
||||
use crate::skills::injection::ToolMentionKind;
|
||||
use crate::skills::injection::app_id_from_path;
|
||||
use crate::skills::injection::tool_kind_for_path;
|
||||
use crate::skills::model::SkillManagedNetworkOverride;
|
||||
use crate::skills::resolve_skill_dependencies_for_turn;
|
||||
use crate::state::ActiveTurn;
|
||||
use crate::state::SessionServices;
|
||||
@@ -1181,6 +1184,61 @@ impl Session {
|
||||
Ok((network_proxy, session_network_proxy))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_or_start_network_proxy(
|
||||
self: &Arc<Self>,
|
||||
scope: NetworkProxyScope,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
managed_network_override: Option<SkillManagedNetworkOverride>,
|
||||
) -> anyhow::Result<Option<NetworkProxy>> {
|
||||
let session = Arc::clone(self);
|
||||
let started = self
|
||||
.services
|
||||
.network_proxies
|
||||
.get_or_start(
|
||||
scope.clone(),
|
||||
move |spec, managed_enabled, audit_metadata| {
|
||||
let session = Arc::clone(&session);
|
||||
let managed_network_override = managed_network_override.clone();
|
||||
let scope = scope.clone();
|
||||
let sandbox_policy = sandbox_policy.clone();
|
||||
async move {
|
||||
let network_policy_decider = session
|
||||
.services
|
||||
.network_policy_decider_session
|
||||
.as_ref()
|
||||
.map(|network_policy_decider_session| {
|
||||
build_network_policy_decider(
|
||||
Arc::clone(&session.services.network_approval),
|
||||
Arc::clone(network_policy_decider_session),
|
||||
scope,
|
||||
)
|
||||
});
|
||||
let spec = if let Some(managed_network_override) =
|
||||
managed_network_override.as_ref()
|
||||
{
|
||||
spec.with_skill_managed_network_override(managed_network_override)
|
||||
} else {
|
||||
spec
|
||||
};
|
||||
spec.start_proxy(
|
||||
&sandbox_policy,
|
||||
network_policy_decider,
|
||||
session
|
||||
.services
|
||||
.network_blocked_request_observer
|
||||
.as_ref()
|
||||
.map(Arc::clone),
|
||||
managed_enabled,
|
||||
audit_metadata,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(started.map(|started| started.proxy()))
|
||||
}
|
||||
|
||||
/// Don't expand the number of mutated arguments on config. We are in the process of getting rid of it.
|
||||
pub(crate) fn build_per_turn_config(session_configuration: &SessionConfiguration) -> Config {
|
||||
// todo(aibrahim): store this state somewhere else so we don't need to mut config
|
||||
@@ -1650,9 +1708,10 @@ impl Session {
|
||||
build_network_policy_decider(
|
||||
Arc::clone(&network_approval),
|
||||
Arc::clone(network_policy_decider_session),
|
||||
NetworkProxyScope::SessionDefault,
|
||||
)
|
||||
});
|
||||
let (network_proxy, session_network_proxy) =
|
||||
let (default_network_proxy, session_network_proxy) =
|
||||
if let Some(spec) = config.permissions.network.as_ref() {
|
||||
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
|
||||
spec,
|
||||
@@ -1660,13 +1719,19 @@ impl Session {
|
||||
network_policy_decider.as_ref().map(Arc::clone),
|
||||
blocked_request_observer.as_ref().map(Arc::clone),
|
||||
managed_network_requirements_enabled,
|
||||
network_proxy_audit_metadata,
|
||||
network_proxy_audit_metadata.clone(),
|
||||
)
|
||||
.await?;
|
||||
(Some(network_proxy), Some(session_network_proxy))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let network_proxies = NetworkProxyRegistry::new(
|
||||
config.permissions.network.clone(),
|
||||
managed_network_requirements_enabled,
|
||||
network_proxy_audit_metadata.clone(),
|
||||
default_network_proxy,
|
||||
);
|
||||
|
||||
let mut hook_shell_argv = default_shell.derive_exec_args("", false);
|
||||
let hook_shell_program = hook_shell_argv.remove(0);
|
||||
@@ -1724,7 +1789,9 @@ impl Session {
|
||||
mcp_manager: Arc::clone(&mcp_manager),
|
||||
file_watcher,
|
||||
agent_control,
|
||||
network_proxy,
|
||||
network_proxies,
|
||||
network_policy_decider_session,
|
||||
network_blocked_request_observer: blocked_request_observer,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: state_db_ctx.clone(),
|
||||
model_client: ModelClient::new(
|
||||
@@ -1763,7 +1830,9 @@ impl Session {
|
||||
js_repl,
|
||||
next_internal_sub_id: AtomicU64::new(0),
|
||||
});
|
||||
if let Some(network_policy_decider_session) = network_policy_decider_session {
|
||||
if let Some(network_policy_decider_session) =
|
||||
sess.services.network_policy_decider_session.as_ref()
|
||||
{
|
||||
let mut guard = network_policy_decider_session.write().await;
|
||||
*guard = Arc::downgrade(&sess);
|
||||
}
|
||||
@@ -2313,8 +2382,10 @@ impl Session {
|
||||
model_info,
|
||||
&self.services.models_manager,
|
||||
self.services
|
||||
.network_proxy
|
||||
.as_ref()
|
||||
.network_proxies
|
||||
.get(&NetworkProxyScope::SessionDefault)
|
||||
.await
|
||||
.as_deref()
|
||||
.map(StartedNetworkProxy::proxy),
|
||||
sub_id,
|
||||
Arc::clone(&self.js_repl),
|
||||
@@ -2686,6 +2757,7 @@ impl Session {
|
||||
&self,
|
||||
amendment: &NetworkPolicyAmendment,
|
||||
network_approval_context: &NetworkApprovalContext,
|
||||
scope: &NetworkProxyScope,
|
||||
) -> anyhow::Result<()> {
|
||||
let host =
|
||||
Self::validated_network_policy_amendment_host(amendment, network_approval_context)?;
|
||||
@@ -2699,7 +2771,7 @@ impl Session {
|
||||
let execpolicy_amendment =
|
||||
execpolicy_network_rule_amendment(amendment, network_approval_context, &host);
|
||||
|
||||
if let Some(started_network_proxy) = self.services.network_proxy.as_ref() {
|
||||
if let Some(started_network_proxy) = self.services.network_proxies.get(scope).await {
|
||||
let proxy = started_network_proxy.proxy();
|
||||
match amendment.action {
|
||||
NetworkPolicyRuleAction::Allow => proxy
|
||||
|
||||
@@ -11,9 +11,11 @@ use crate::exec::ExecToolCallOutput;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use crate::models_manager::model_info;
|
||||
use crate::network_proxy_registry::NetworkProxyRegistry;
|
||||
use crate::shell::default_user_shell;
|
||||
use crate::tools::format_exec_output_str;
|
||||
|
||||
use codex_network_proxy::NetworkProxyAuditMetadata;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
@@ -2151,7 +2153,14 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
mcp_manager,
|
||||
file_watcher,
|
||||
agent_control,
|
||||
network_proxy: None,
|
||||
network_proxies: NetworkProxyRegistry::new(
|
||||
None,
|
||||
false,
|
||||
NetworkProxyAuditMetadata::default(),
|
||||
None,
|
||||
),
|
||||
network_policy_decider_session: None,
|
||||
network_blocked_request_observer: None,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: None,
|
||||
model_client: ModelClient::new(
|
||||
@@ -2793,7 +2802,14 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
mcp_manager,
|
||||
file_watcher,
|
||||
agent_control,
|
||||
network_proxy: None,
|
||||
network_proxies: NetworkProxyRegistry::new(
|
||||
None,
|
||||
false,
|
||||
NetworkProxyAuditMetadata::default(),
|
||||
None,
|
||||
),
|
||||
network_policy_decider_session: None,
|
||||
network_blocked_request_observer: None,
|
||||
network_approval: Arc::clone(&network_approval),
|
||||
state_db: None,
|
||||
model_client: ModelClient::new(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::config_loader::NetworkConstraints;
|
||||
use crate::skills::model::SkillManagedNetworkOverride;
|
||||
use async_trait::async_trait;
|
||||
use codex_network_proxy::BlockedRequestObserver;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
@@ -81,6 +82,28 @@ impl NetworkProxySpec {
|
||||
self.config.network.enable_socks5
|
||||
}
|
||||
|
||||
pub(crate) fn with_skill_managed_network_override(
|
||||
&self,
|
||||
managed_network_override: &SkillManagedNetworkOverride,
|
||||
) -> Self {
|
||||
let mut spec = self.clone();
|
||||
|
||||
if let Some(allowed_domains) = managed_network_override.allowed_domains.clone() {
|
||||
spec.config.network.allowed_domains = allowed_domains.clone();
|
||||
if spec.constraints.allowed_domains.is_some() {
|
||||
spec.constraints.allowed_domains = Some(allowed_domains);
|
||||
}
|
||||
}
|
||||
if let Some(denied_domains) = managed_network_override.denied_domains.clone() {
|
||||
spec.config.network.denied_domains = denied_domains.clone();
|
||||
if spec.constraints.denied_domains.is_some() {
|
||||
spec.constraints.denied_domains = Some(denied_domains);
|
||||
}
|
||||
}
|
||||
|
||||
spec
|
||||
}
|
||||
|
||||
pub(crate) fn from_config_and_constraints(
|
||||
config: NetworkProxyConfig,
|
||||
requirements: Option<NetworkConstraints>,
|
||||
|
||||
@@ -1,6 +1,94 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn skill_managed_network_override_replaces_allowed_domains_and_keeps_other_settings() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.enabled = true;
|
||||
config.network.proxy_url = "http://127.0.0.1:4128".to_string();
|
||||
config.network.socks_url = "socks5://127.0.0.1:5128".to_string();
|
||||
config.network.enable_socks5 = true;
|
||||
config.network.enable_socks5_udp = true;
|
||||
config.network.allowed_domains = vec!["default.example.com".to_string()];
|
||||
config.network.denied_domains = vec!["blocked.example.com".to_string()];
|
||||
config.network.allow_upstream_proxy = true;
|
||||
config.network.dangerously_allow_all_unix_sockets = false;
|
||||
config.network.dangerously_allow_non_loopback_proxy = false;
|
||||
config.network.mode = codex_network_proxy::NetworkMode::Full;
|
||||
config.network.allow_unix_sockets = vec!["/tmp/default.sock".to_string()];
|
||||
config.network.allow_local_binding = true;
|
||||
config.network.mitm = false;
|
||||
let spec = NetworkProxySpec {
|
||||
config,
|
||||
constraints: NetworkProxyConstraints {
|
||||
allowed_domains: Some(vec!["default.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
allowlist_expansion_enabled: Some(true),
|
||||
denylist_expansion_enabled: Some(false),
|
||||
allow_upstream_proxy: Some(true),
|
||||
allow_unix_sockets: Some(vec!["/tmp/default.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
..NetworkProxyConstraints::default()
|
||||
},
|
||||
hard_deny_allowlist_misses: true,
|
||||
};
|
||||
let managed_network_override = crate::skills::model::SkillManagedNetworkOverride {
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: None,
|
||||
};
|
||||
|
||||
let overridden = spec.with_skill_managed_network_override(&managed_network_override);
|
||||
|
||||
let mut expected = spec.clone();
|
||||
expected.config.network.allowed_domains = vec!["skill.example.com".to_string()];
|
||||
expected.constraints.allowed_domains = Some(vec!["skill.example.com".to_string()]);
|
||||
assert_eq!(overridden, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_managed_network_override_replaces_denied_domains_and_keeps_default_allowed_domains() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config.network.enabled = true;
|
||||
config.network.proxy_url = "http://127.0.0.1:4128".to_string();
|
||||
config.network.socks_url = "socks5://127.0.0.1:5128".to_string();
|
||||
config.network.enable_socks5 = true;
|
||||
config.network.enable_socks5_udp = true;
|
||||
config.network.allowed_domains = vec!["default.example.com".to_string()];
|
||||
config.network.denied_domains = vec!["blocked.example.com".to_string()];
|
||||
config.network.allow_upstream_proxy = true;
|
||||
config.network.dangerously_allow_all_unix_sockets = false;
|
||||
config.network.dangerously_allow_non_loopback_proxy = false;
|
||||
config.network.mode = codex_network_proxy::NetworkMode::Full;
|
||||
config.network.allow_unix_sockets = vec!["/tmp/default.sock".to_string()];
|
||||
config.network.allow_local_binding = true;
|
||||
config.network.mitm = false;
|
||||
let spec = NetworkProxySpec {
|
||||
config,
|
||||
constraints: NetworkProxyConstraints {
|
||||
allowed_domains: Some(vec!["default.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.example.com".to_string()]),
|
||||
allowlist_expansion_enabled: Some(true),
|
||||
denylist_expansion_enabled: Some(false),
|
||||
allow_upstream_proxy: Some(true),
|
||||
allow_unix_sockets: Some(vec!["/tmp/default.sock".to_string()]),
|
||||
allow_local_binding: Some(true),
|
||||
..NetworkProxyConstraints::default()
|
||||
},
|
||||
hard_deny_allowlist_misses: false,
|
||||
};
|
||||
let managed_network_override = crate::skills::model::SkillManagedNetworkOverride {
|
||||
allowed_domains: None,
|
||||
denied_domains: Some(vec!["skill-blocked.example.com".to_string()]),
|
||||
};
|
||||
|
||||
let overridden = spec.with_skill_managed_network_override(&managed_network_override);
|
||||
|
||||
let mut expected = spec.clone();
|
||||
expected.config.network.denied_domains = vec!["skill-blocked.example.com".to_string()];
|
||||
expected.constraints.denied_domains = Some(vec!["skill-blocked.example.com".to_string()]);
|
||||
assert_eq!(overridden, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_state_with_audit_metadata_threads_metadata_to_state() {
|
||||
let spec = NetworkProxySpec {
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::config::Constrained;
|
||||
use crate::config::NetworkProxySpec;
|
||||
use crate::event_mapping::is_contextual_user_message_content;
|
||||
use crate::features::Feature;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
use crate::protocol::Op;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::truncate::approx_bytes_for_tokens;
|
||||
@@ -550,7 +551,12 @@ async fn run_guardian_subagent(
|
||||
schema: Value,
|
||||
cancel_token: CancellationToken,
|
||||
) -> anyhow::Result<GuardianAssessment> {
|
||||
let live_network_config = match session.services.network_proxy.as_ref() {
|
||||
let live_network_config = match session
|
||||
.services
|
||||
.network_proxies
|
||||
.get(&NetworkProxyScope::SessionDefault)
|
||||
.await
|
||||
{
|
||||
Some(network_proxy) => Some(network_proxy.proxy().current_cfg().await?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ mod mcp_tool_approval_templates;
|
||||
pub mod models_manager;
|
||||
mod network_policy_decision;
|
||||
pub mod network_proxy_loader;
|
||||
mod network_proxy_registry;
|
||||
mod original_image_detail;
|
||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
|
||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
|
||||
|
||||
@@ -13,6 +13,7 @@ fn skill_with_tools(tools: Vec<SkillToolDependency>) -> SkillMetadata {
|
||||
dependencies: Some(SkillDependencies { tools }),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: PathBuf::from("skill"),
|
||||
scope: SkillScope::User,
|
||||
}
|
||||
|
||||
77
codex-rs/core/src/network_proxy_registry.rs
Normal file
77
codex-rs/core/src/network_proxy_registry.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use crate::config::NetworkProxySpec;
|
||||
use crate::config::StartedNetworkProxy;
|
||||
use anyhow::Result;
|
||||
use codex_network_proxy::NetworkProxyAuditMetadata;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub(crate) enum NetworkProxyScope {
|
||||
SessionDefault,
|
||||
Skill { path_to_skills_md: PathBuf },
|
||||
}
|
||||
|
||||
pub(crate) struct NetworkProxyRegistry {
|
||||
spec: Option<NetworkProxySpec>,
|
||||
managed_network_requirements_enabled: bool,
|
||||
audit_metadata: NetworkProxyAuditMetadata,
|
||||
proxies: Mutex<HashMap<NetworkProxyScope, Arc<StartedNetworkProxy>>>,
|
||||
}
|
||||
|
||||
impl NetworkProxyRegistry {
|
||||
pub(crate) fn new(
|
||||
spec: Option<NetworkProxySpec>,
|
||||
managed_network_requirements_enabled: bool,
|
||||
audit_metadata: NetworkProxyAuditMetadata,
|
||||
default_proxy: Option<StartedNetworkProxy>,
|
||||
) -> Self {
|
||||
let mut proxies = HashMap::new();
|
||||
if let Some(default_proxy) = default_proxy {
|
||||
proxies.insert(NetworkProxyScope::SessionDefault, Arc::new(default_proxy));
|
||||
}
|
||||
|
||||
Self {
|
||||
spec,
|
||||
managed_network_requirements_enabled,
|
||||
audit_metadata,
|
||||
proxies: Mutex::new(proxies),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get(&self, scope: &NetworkProxyScope) -> Option<Arc<StartedNetworkProxy>> {
|
||||
self.proxies.lock().await.get(scope).cloned()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_or_start<F, Fut>(
|
||||
&self,
|
||||
scope: NetworkProxyScope,
|
||||
start: F,
|
||||
) -> Result<Option<Arc<StartedNetworkProxy>>>
|
||||
where
|
||||
F: FnOnce(NetworkProxySpec, bool, NetworkProxyAuditMetadata) -> Fut,
|
||||
Fut: Future<Output = std::io::Result<StartedNetworkProxy>>,
|
||||
{
|
||||
let mut proxies = self.proxies.lock().await;
|
||||
if let Some(existing) = proxies.get(&scope).cloned() {
|
||||
return Ok(Some(existing));
|
||||
}
|
||||
|
||||
let Some(spec) = self.spec.clone() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let started = Arc::new(
|
||||
start(
|
||||
spec,
|
||||
self.managed_network_requirements_enabled,
|
||||
self.audit_metadata.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
proxies.insert(scope, Arc::clone(&started));
|
||||
Ok(Some(started))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ fn make_skill(name: &str, path: &str) -> SkillMetadata {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: PathBuf::from(path),
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ fn test_skill_metadata(skill_doc_path: PathBuf) -> SkillMetadata {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: skill_doc_path,
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ use crate::skills::model::SkillDependencies;
|
||||
use crate::skills::model::SkillError;
|
||||
use crate::skills::model::SkillInterface;
|
||||
use crate::skills::model::SkillLoadOutcome;
|
||||
use crate::skills::model::SkillManagedNetworkOverride;
|
||||
use crate::skills::model::SkillMetadata;
|
||||
use crate::skills::model::SkillPolicy;
|
||||
use crate::skills::model::SkillToolDependency;
|
||||
use crate::skills::system::system_cache_root_dir;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsSeatbeltProfileExtensions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
@@ -32,8 +36,6 @@ use tracing::error;
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::config::Config;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SkillFrontmatter {
|
||||
@@ -60,7 +62,7 @@ struct SkillMetadataFile {
|
||||
#[serde(default)]
|
||||
policy: Option<Policy>,
|
||||
#[serde(default)]
|
||||
permissions: Option<PermissionProfile>,
|
||||
permissions: Option<SkillPermissionProfile>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -69,6 +71,27 @@ struct LoadedSkillMetadata {
|
||||
dependencies: Option<SkillDependencies>,
|
||||
policy: Option<SkillPolicy>,
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
managed_network_override: Option<SkillManagedNetworkOverride>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
||||
struct SkillPermissionProfile {
|
||||
#[serde(default)]
|
||||
network: Option<SkillNetworkPermissions>,
|
||||
#[serde(default)]
|
||||
file_system: Option<FileSystemPermissions>,
|
||||
#[serde(default)]
|
||||
macos: Option<MacOsSeatbeltProfileExtensions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
|
||||
struct SkillNetworkPermissions {
|
||||
#[serde(default)]
|
||||
enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
allowed_domains: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
denied_domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -527,6 +550,7 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
dependencies,
|
||||
policy,
|
||||
permission_profile,
|
||||
managed_network_override,
|
||||
} = load_skill_metadata(path);
|
||||
|
||||
validate_len(&name, MAX_NAME_LEN, "name")?;
|
||||
@@ -549,6 +573,7 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
dependencies,
|
||||
policy,
|
||||
permission_profile,
|
||||
managed_network_override,
|
||||
path_to_skills_md: resolved_path,
|
||||
scope,
|
||||
})
|
||||
@@ -614,14 +639,52 @@ fn load_skill_metadata(skill_path: &Path) -> LoadedSkillMetadata {
|
||||
policy,
|
||||
permissions,
|
||||
} = parsed;
|
||||
let (permission_profile, managed_network_override) = normalize_permissions(permissions);
|
||||
LoadedSkillMetadata {
|
||||
interface: resolve_interface(interface, skill_dir),
|
||||
dependencies: resolve_dependencies(dependencies),
|
||||
policy: resolve_policy(policy),
|
||||
permission_profile: permissions.filter(|profile| !profile.is_empty()),
|
||||
permission_profile,
|
||||
managed_network_override,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_permissions(
|
||||
permissions: Option<SkillPermissionProfile>,
|
||||
) -> (
|
||||
Option<PermissionProfile>,
|
||||
Option<SkillManagedNetworkOverride>,
|
||||
) {
|
||||
let Some(permissions) = permissions else {
|
||||
return (None, None);
|
||||
};
|
||||
let managed_network_override = permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.map(|network| SkillManagedNetworkOverride {
|
||||
allowed_domains: network.allowed_domains.clone(),
|
||||
denied_domains: network.denied_domains.clone(),
|
||||
})
|
||||
.filter(SkillManagedNetworkOverride::has_domain_overrides);
|
||||
let permission_profile = PermissionProfile {
|
||||
network: permissions.network.and_then(|network| {
|
||||
let network = NetworkPermissions {
|
||||
enabled: network.enabled,
|
||||
};
|
||||
(!network.is_empty()).then_some(network)
|
||||
}),
|
||||
file_system: permissions
|
||||
.file_system
|
||||
.filter(|file_system| !file_system.is_empty()),
|
||||
macos: permissions.macos,
|
||||
};
|
||||
|
||||
(
|
||||
(!permission_profile.is_empty()).then_some(permission_profile),
|
||||
managed_network_override,
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_interface(interface: Option<Interface>, skill_dir: &Path) -> Option<SkillInterface> {
|
||||
let interface = interface?;
|
||||
let interface = SkillInterface {
|
||||
|
||||
@@ -239,6 +239,7 @@ fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -390,6 +391,7 @@ async fn loads_skill_dependencies_metadata_from_yaml() {
|
||||
}),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -446,6 +448,7 @@ interface:
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(skill_path.as_path()),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -568,6 +571,7 @@ permissions:
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(outcome.skills[0].managed_network_override, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -595,6 +599,70 @@ permissions: {}
|
||||
assert_eq!(outcome.skills[0].permission_profile, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_permissions_splits_managed_network_overrides() {
|
||||
let (permission_profile, managed_network_override) =
|
||||
normalize_permissions(Some(SkillPermissionProfile {
|
||||
network: Some(SkillNetworkPermissions {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.skill.example.com".to_string()]),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
permission_profile,
|
||||
Some(PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
managed_network_override,
|
||||
Some(SkillManagedNetworkOverride {
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.skill.example.com".to_string()]),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_permissions_preserves_network_gate_separately_from_overrides() {
|
||||
let (permission_profile, managed_network_override) =
|
||||
normalize_permissions(Some(SkillPermissionProfile {
|
||||
network: Some(SkillNetworkPermissions {
|
||||
enabled: Some(false),
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: None,
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
permission_profile,
|
||||
Some(PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(false),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
managed_network_override,
|
||||
Some(SkillManagedNetworkOverride {
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_metadata_parses_macos_permissions_yaml() {
|
||||
let parsed = serde_yaml::from_str::<SkillMetadataFile>(
|
||||
@@ -613,7 +681,9 @@ permissions:
|
||||
|
||||
assert_eq!(
|
||||
parsed.permissions,
|
||||
Some(PermissionProfile {
|
||||
Some(SkillPermissionProfile {
|
||||
network: None,
|
||||
file_system: None,
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadWrite,
|
||||
macos_automation: MacOsAutomationPermission::BundleIds(vec![
|
||||
@@ -625,7 +695,6 @@ permissions:
|
||||
macos_reminders: false,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -643,7 +712,9 @@ permissions:
|
||||
|
||||
assert_eq!(
|
||||
parsed.permissions,
|
||||
Some(PermissionProfile {
|
||||
Some(SkillPermissionProfile {
|
||||
network: None,
|
||||
file_system: None,
|
||||
macos: Some(MacOsSeatbeltProfileExtensions {
|
||||
macos_preferences: MacOsPreferencesPermission::ReadOnly,
|
||||
macos_automation: MacOsAutomationPermission::None,
|
||||
@@ -653,7 +724,35 @@ permissions:
|
||||
macos_reminders: true,
|
||||
macos_contacts: MacOsContactsPermission::None,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_metadata_parses_network_domain_overrides_under_permissions() {
|
||||
let parsed = serde_yaml::from_str::<SkillMetadataFile>(
|
||||
r#"
|
||||
permissions:
|
||||
network:
|
||||
enabled: true
|
||||
allowed_domains:
|
||||
- "skill.example.com"
|
||||
denied_domains:
|
||||
- "blocked.skill.example.com"
|
||||
"#,
|
||||
)
|
||||
.expect("parse network skill metadata");
|
||||
|
||||
assert_eq!(
|
||||
parsed.permissions,
|
||||
Some(SkillPermissionProfile {
|
||||
network: Some(SkillNetworkPermissions {
|
||||
enabled: Some(true),
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.skill.example.com".to_string()]),
|
||||
}),
|
||||
file_system: None,
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -801,6 +900,7 @@ async fn accepts_icon_paths_under_assets_dir() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -842,6 +942,7 @@ async fn ignores_invalid_brand_color() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -896,6 +997,7 @@ async fn ignores_default_prompt_over_max_length() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -938,6 +1040,7 @@ async fn drops_interface_when_icons_are_invalid() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -983,6 +1086,7 @@ async fn loads_skills_via_symlinked_subdir_for_user_scope() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&shared_skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1043,6 +1147,7 @@ async fn does_not_loop_on_symlink_cycle_for_user_scope() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1080,6 +1185,7 @@ fn loads_skills_via_symlinked_subdir_for_admin_scope() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&shared_skill_path),
|
||||
scope: SkillScope::Admin,
|
||||
}]
|
||||
@@ -1120,6 +1226,7 @@ async fn loads_skills_via_symlinked_subdir_for_repo_scope() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&linked_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -1188,6 +1295,7 @@ async fn respects_max_scan_depth_for_user_scope() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&within_depth_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1216,6 +1324,7 @@ async fn loads_valid_skill() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1249,6 +1358,7 @@ async fn falls_back_to_directory_name_when_skill_name_is_missing() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1291,6 +1401,7 @@ async fn namespaces_plugin_skills_using_plugin_name() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1323,6 +1434,7 @@ async fn loads_short_description_from_metadata() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
}]
|
||||
@@ -1436,6 +1548,7 @@ async fn loads_skills_from_repo_root() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -1472,6 +1585,7 @@ async fn loads_skills_from_agents_dir_without_codex_dir() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -1526,6 +1640,7 @@ async fn loads_skills_from_all_codex_dirs_under_project_root() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&nested_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -1537,6 +1652,7 @@ async fn loads_skills_from_all_codex_dirs_under_project_root() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&root_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -1577,6 +1693,7 @@ async fn loads_skills_from_codex_dir_when_not_git_repo() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -1615,6 +1732,7 @@ async fn deduplicates_by_path_preferring_first_root() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -1657,6 +1775,7 @@ async fn keeps_duplicate_names_from_repo_and_user() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&repo_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -1668,6 +1787,7 @@ async fn keeps_duplicate_names_from_repo_and_user() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&user_skill_path),
|
||||
scope: SkillScope::User,
|
||||
},
|
||||
@@ -1732,6 +1852,7 @@ async fn keeps_duplicate_names_from_nested_codex_dirs() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: first_path,
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -1743,6 +1864,7 @@ async fn keeps_duplicate_names_from_nested_codex_dirs() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: second_path,
|
||||
scope: SkillScope::Repo,
|
||||
},
|
||||
@@ -1815,6 +1937,7 @@ async fn loads_skills_when_cwd_is_file_in_repo() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
}]
|
||||
@@ -1874,6 +1997,7 @@ async fn loads_skills_from_system_cache_when_present() {
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::System,
|
||||
}]
|
||||
|
||||
@@ -5,6 +5,19 @@ use std::sync::Arc;
|
||||
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
pub struct SkillManagedNetworkOverride {
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl SkillManagedNetworkOverride {
|
||||
pub fn has_domain_overrides(&self) -> bool {
|
||||
self.allowed_domains.is_some() || self.denied_domains.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SkillMetadata {
|
||||
@@ -15,6 +28,7 @@ pub struct SkillMetadata {
|
||||
pub dependencies: Option<SkillDependencies>,
|
||||
pub policy: Option<SkillPolicy>,
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub managed_network_override: Option<SkillManagedNetworkOverride>,
|
||||
/// Path to the SKILLS.md file that declares this skill.
|
||||
pub path_to_skills_md: PathBuf,
|
||||
pub scope: SkillScope,
|
||||
|
||||
@@ -6,12 +6,13 @@ use crate::RolloutRecorder;
|
||||
use crate::agent::AgentControl;
|
||||
use crate::analytics_client::AnalyticsEventsClient;
|
||||
use crate::client::ModelClient;
|
||||
use crate::config::StartedNetworkProxy;
|
||||
use crate::codex::Session;
|
||||
use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::file_watcher::FileWatcher;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use crate::network_proxy_registry::NetworkProxyRegistry;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::skills::SkillsManager;
|
||||
use crate::state_db::StateDbHandle;
|
||||
@@ -21,9 +22,11 @@ use crate::tools::runtimes::ExecveSessionApproval;
|
||||
use crate::tools::sandboxing::ApprovalStore;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use codex_hooks::Hooks;
|
||||
use codex_network_proxy::BlockedRequestObserver;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Weak;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::watch;
|
||||
@@ -55,7 +58,9 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) mcp_manager: Arc<McpManager>,
|
||||
pub(crate) file_watcher: Arc<FileWatcher>,
|
||||
pub(crate) agent_control: AgentControl,
|
||||
pub(crate) network_proxy: Option<StartedNetworkProxy>,
|
||||
pub(crate) network_proxies: NetworkProxyRegistry,
|
||||
pub(crate) network_policy_decider_session: Option<Arc<RwLock<Weak<Session>>>>,
|
||||
pub(crate) network_blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
|
||||
pub(crate) network_approval: Arc<NetworkApprovalService>,
|
||||
pub(crate) state_db: Option<StateDbHandle>,
|
||||
/// Session-scoped model client shared across turns.
|
||||
|
||||
@@ -46,6 +46,7 @@ use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
|
||||
use crate::features::Feature;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
pub(crate) use compact::CompactTask;
|
||||
pub(crate) use ghost_snapshot::GhostSnapshotTask;
|
||||
pub(crate) use regular::RegularTask;
|
||||
@@ -295,7 +296,12 @@ impl Session {
|
||||
"false"
|
||||
},
|
||||
);
|
||||
let network_proxy_active = match self.services.network_proxy.as_ref() {
|
||||
let network_proxy_active = match self
|
||||
.services
|
||||
.network_proxies
|
||||
.get(&NetworkProxyScope::SessionDefault)
|
||||
.await
|
||||
{
|
||||
Some(started_network_proxy) => {
|
||||
match started_network_proxy.proxy().current_cfg().await {
|
||||
Ok(config) => config.network.enabled,
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_policy_decision::denied_network_policy_message;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use codex_network_proxy::BlockedRequest;
|
||||
use codex_network_proxy::BlockedRequestObserver;
|
||||
@@ -76,14 +77,20 @@ impl ActiveNetworkApproval {
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct HostApprovalKey {
|
||||
scope: NetworkProxyScope,
|
||||
host: String,
|
||||
protocol: &'static str,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl HostApprovalKey {
|
||||
fn from_request(request: &NetworkPolicyRequest, protocol: NetworkApprovalProtocol) -> Self {
|
||||
fn from_request(
|
||||
request: &NetworkPolicyRequest,
|
||||
protocol: NetworkApprovalProtocol,
|
||||
scope: NetworkProxyScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
scope,
|
||||
host: request.host.to_ascii_lowercase(),
|
||||
protocol: protocol_key_label(protocol),
|
||||
port: request.port,
|
||||
@@ -279,6 +286,7 @@ impl NetworkApprovalService {
|
||||
&self,
|
||||
session: Arc<Session>,
|
||||
request: NetworkPolicyRequest,
|
||||
scope: NetworkProxyScope,
|
||||
) -> NetworkDecision {
|
||||
const REASON_NOT_ALLOWED: &str = "not_allowed";
|
||||
|
||||
@@ -288,7 +296,7 @@ impl NetworkApprovalService {
|
||||
NetworkProtocol::Socks5Tcp => NetworkApprovalProtocol::Socks5Tcp,
|
||||
NetworkProtocol::Socks5Udp => NetworkApprovalProtocol::Socks5Udp,
|
||||
};
|
||||
let key = HostApprovalKey::from_request(&request, protocol);
|
||||
let key = HostApprovalKey::from_request(&request, protocol, scope.clone());
|
||||
|
||||
{
|
||||
let denied_hosts = self.session_denied_hosts.lock().await;
|
||||
@@ -387,6 +395,7 @@ impl NetworkApprovalService {
|
||||
.persist_network_policy_amendment(
|
||||
&network_policy_amendment,
|
||||
&network_approval_context,
|
||||
&scope,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -417,6 +426,7 @@ impl NetworkApprovalService {
|
||||
.persist_network_policy_amendment(
|
||||
&network_policy_amendment,
|
||||
&network_approval_context,
|
||||
&scope,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -506,16 +516,18 @@ pub(crate) fn build_blocked_request_observer(
|
||||
pub(crate) fn build_network_policy_decider(
|
||||
network_approval: Arc<NetworkApprovalService>,
|
||||
network_policy_decider_session: Arc<RwLock<std::sync::Weak<Session>>>,
|
||||
scope: NetworkProxyScope,
|
||||
) -> Arc<dyn NetworkPolicyDecider> {
|
||||
Arc::new(move |request: NetworkPolicyRequest| {
|
||||
let network_approval = Arc::clone(&network_approval);
|
||||
let network_policy_decider_session = Arc::clone(&network_policy_decider_session);
|
||||
let scope = scope.clone();
|
||||
async move {
|
||||
let Some(session) = network_policy_decider_session.read().await.upgrade() else {
|
||||
return NetworkDecision::ask("not_allowed");
|
||||
};
|
||||
network_approval
|
||||
.handle_inline_policy_request(session, request)
|
||||
.handle_inline_policy_request(session, request, scope)
|
||||
.await
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
use codex_network_proxy::BlockedRequestArgs;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -7,6 +8,7 @@ use pretty_assertions::assert_eq;
|
||||
async fn pending_approvals_are_deduped_per_host_protocol_and_port() {
|
||||
let service = NetworkApprovalService::default();
|
||||
let key = HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "http",
|
||||
port: 443,
|
||||
@@ -24,11 +26,13 @@ async fn pending_approvals_are_deduped_per_host_protocol_and_port() {
|
||||
async fn pending_approvals_do_not_dedupe_across_ports() {
|
||||
let service = NetworkApprovalService::default();
|
||||
let first_key = HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "https",
|
||||
port: 443,
|
||||
};
|
||||
let second_key = HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "https",
|
||||
port: 8443,
|
||||
@@ -49,16 +53,19 @@ async fn session_approved_hosts_preserve_protocol_and_port_scope() {
|
||||
let mut approved_hosts = source.session_approved_hosts.lock().await;
|
||||
approved_hosts.extend([
|
||||
HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "https",
|
||||
port: 443,
|
||||
},
|
||||
HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "https",
|
||||
port: 8443,
|
||||
},
|
||||
HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "http",
|
||||
port: 80,
|
||||
@@ -82,16 +89,19 @@ async fn session_approved_hosts_preserve_protocol_and_port_scope() {
|
||||
copied,
|
||||
vec![
|
||||
HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "http",
|
||||
port: 80,
|
||||
},
|
||||
HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "https",
|
||||
port: 443,
|
||||
},
|
||||
HostApprovalKey {
|
||||
scope: NetworkProxyScope::SessionDefault,
|
||||
host: "example.com".to_string(),
|
||||
protocol: "https",
|
||||
port: 8443,
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::features::Feature;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
@@ -50,6 +51,7 @@ use codex_shell_escalation::ShellCommandExecutor;
|
||||
use codex_shell_escalation::Stopwatch;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -142,6 +144,7 @@ pub(super) async fn try_run_zsh_fork(
|
||||
ctx.session.services.exec_policy.current().as_ref().clone(),
|
||||
));
|
||||
let command_executor = CoreShellCommandExecutor {
|
||||
session: Some(Arc::clone(&ctx.session)),
|
||||
command,
|
||||
cwd: sandbox_cwd,
|
||||
sandbox_policy,
|
||||
@@ -259,6 +262,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
ctx.session.services.exec_policy.current().as_ref().clone(),
|
||||
));
|
||||
let command_executor = CoreShellCommandExecutor {
|
||||
session: Some(Arc::clone(&ctx.session)),
|
||||
command: exec_request.command.clone(),
|
||||
cwd: exec_request.cwd.clone(),
|
||||
sandbox_policy: exec_request.sandbox_policy.clone(),
|
||||
@@ -503,26 +507,7 @@ impl CoreShellActionProvider {
|
||||
/// an absolute path. The idea is that we check to see whether it matches
|
||||
/// any skills.
|
||||
async fn find_skill(&self, program: &AbsolutePathBuf) -> Option<SkillMetadata> {
|
||||
let force_reload = false;
|
||||
let skills_outcome = self
|
||||
.session
|
||||
.services
|
||||
.skills_manager
|
||||
.skills_for_cwd(&self.turn.cwd, force_reload)
|
||||
.await;
|
||||
|
||||
let program_path = program.as_path();
|
||||
for skill in skills_outcome.skills {
|
||||
// We intentionally ignore "enabled" status here for now.
|
||||
let Some(skill_root) = skill.path_to_skills_md.parent() else {
|
||||
continue;
|
||||
};
|
||||
if program_path.starts_with(skill_root.join("scripts")) {
|
||||
return Some(skill);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
find_skill_for_program(self.session.as_ref(), self.turn.cwd.as_path(), program).await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -631,6 +616,32 @@ impl CoreShellActionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_skill_for_program(
|
||||
session: &crate::codex::Session,
|
||||
cwd: &Path,
|
||||
program: &AbsolutePathBuf,
|
||||
) -> Option<SkillMetadata> {
|
||||
let force_reload = false;
|
||||
let skills_outcome = session
|
||||
.services
|
||||
.skills_manager
|
||||
.skills_for_cwd(cwd, force_reload)
|
||||
.await;
|
||||
|
||||
let program_path = program.as_path();
|
||||
for skill in skills_outcome.skills {
|
||||
// We intentionally ignore "enabled" status here for now.
|
||||
let Some(skill_root) = skill.path_to_skills_md.parent() else {
|
||||
continue;
|
||||
};
|
||||
if program_path.starts_with(skill_root.join("scripts")) {
|
||||
return Some(skill);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// Shell-wrapper parsing is weaker than direct exec interception because it can
|
||||
// only see the script text, not the final resolved executable path. Keep it
|
||||
// disabled by default so path-sensitive rules rely on the later authoritative
|
||||
@@ -865,6 +876,7 @@ fn commands_for_intercepted_exec_policy(
|
||||
}
|
||||
|
||||
struct CoreShellCommandExecutor {
|
||||
session: Option<Arc<crate::codex::Session>>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
@@ -888,6 +900,7 @@ struct PrepareSandboxedExecParams<'a> {
|
||||
command: Vec<String>,
|
||||
workdir: &'a AbsolutePathBuf,
|
||||
env: HashMap<String, String>,
|
||||
network: Option<codex_network_proxy::NetworkProxy>,
|
||||
sandbox_policy: &'a SandboxPolicy,
|
||||
file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
@@ -969,10 +982,12 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
arg0: Some(first_arg.clone()),
|
||||
},
|
||||
EscalationExecution::TurnDefault => {
|
||||
let network = self.network_for_program(program).await?;
|
||||
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
|
||||
command,
|
||||
workdir,
|
||||
env,
|
||||
network,
|
||||
sandbox_policy: &self.sandbox_policy,
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
@@ -986,12 +1001,14 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
EscalationExecution::Permissions(EscalationPermissions::PermissionProfile(
|
||||
permission_profile,
|
||||
)) => {
|
||||
let network = self.network_for_program(program).await?;
|
||||
// Merge additive permissions into the existing turn/request sandbox policy.
|
||||
// On macOS, additional profile extensions are unioned with the turn defaults.
|
||||
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
|
||||
command,
|
||||
workdir,
|
||||
env,
|
||||
network,
|
||||
sandbox_policy: &self.sandbox_policy,
|
||||
file_system_sandbox_policy: &self.file_system_sandbox_policy,
|
||||
network_sandbox_policy: self.network_sandbox_policy,
|
||||
@@ -1003,11 +1020,13 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
})?
|
||||
}
|
||||
EscalationExecution::Permissions(EscalationPermissions::Permissions(permissions)) => {
|
||||
let network = self.network_for_program(program).await?;
|
||||
// Use a fully specified sandbox policy instead of merging into the turn policy.
|
||||
self.prepare_sandboxed_exec(PrepareSandboxedExecParams {
|
||||
command,
|
||||
workdir,
|
||||
env,
|
||||
network,
|
||||
sandbox_policy: &permissions.sandbox_policy,
|
||||
file_system_sandbox_policy: &permissions.file_system_sandbox_policy,
|
||||
network_sandbox_policy: permissions.network_sandbox_policy,
|
||||
@@ -1025,6 +1044,25 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
}
|
||||
|
||||
impl CoreShellCommandExecutor {
|
||||
async fn network_for_program(
|
||||
&self,
|
||||
program: &AbsolutePathBuf,
|
||||
) -> anyhow::Result<Option<codex_network_proxy::NetworkProxy>> {
|
||||
let Some(session) = self.session.as_ref() else {
|
||||
return Ok(self.network.clone());
|
||||
};
|
||||
let Some(skill) =
|
||||
find_skill_for_program(session.as_ref(), &self.sandbox_policy_cwd, program).await
|
||||
else {
|
||||
return Ok(self.network.clone());
|
||||
};
|
||||
let (scope, managed_network_override) = network_proxy_scope_for_skill(&skill);
|
||||
|
||||
session
|
||||
.get_or_start_network_proxy(scope, &self.sandbox_policy, managed_network_override)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepare_sandboxed_exec(
|
||||
&self,
|
||||
@@ -1034,6 +1072,7 @@ impl CoreShellCommandExecutor {
|
||||
command,
|
||||
workdir,
|
||||
env,
|
||||
network,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -1050,7 +1089,7 @@ impl CoreShellCommandExecutor {
|
||||
network_sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
self.windows_sandbox_level,
|
||||
self.network.is_some(),
|
||||
network.is_some(),
|
||||
);
|
||||
let mut exec_request =
|
||||
sandbox_manager.transform(crate::sandboxing::SandboxTransformRequest {
|
||||
@@ -1072,8 +1111,8 @@ impl CoreShellCommandExecutor {
|
||||
file_system_policy: file_system_sandbox_policy,
|
||||
network_policy: network_sandbox_policy,
|
||||
sandbox,
|
||||
enforce_managed_network: self.network.is_some(),
|
||||
network: self.network.as_ref(),
|
||||
enforce_managed_network: network.is_some(),
|
||||
network: network.as_ref(),
|
||||
sandbox_policy_cwd: &self.sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
@@ -1094,6 +1133,23 @@ impl CoreShellCommandExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
fn network_proxy_scope_for_skill(
|
||||
skill: &SkillMetadata,
|
||||
) -> (
|
||||
NetworkProxyScope,
|
||||
Option<crate::skills::model::SkillManagedNetworkOverride>,
|
||||
) {
|
||||
match skill.managed_network_override.clone() {
|
||||
Some(managed_network_override) => (
|
||||
NetworkProxyScope::Skill {
|
||||
path_to_skills_md: skill.path_to_skills_md.clone(),
|
||||
},
|
||||
Some(managed_network_override),
|
||||
),
|
||||
None => (NetworkProxyScope::SessionDefault, None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct ParsedShellCommand {
|
||||
program: String,
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::config::Permissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::exec::SandboxType;
|
||||
use crate::network_proxy_registry::NetworkProxyScope;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
use crate::protocol::RejectConfig;
|
||||
@@ -92,11 +93,41 @@ fn test_skill_metadata(permission_profile: Option<PermissionProfile>) -> SkillMe
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile,
|
||||
managed_network_override: None,
|
||||
path_to_skills_md: PathBuf::from("/tmp/skill/SKILL.md"),
|
||||
scope: SkillScope::User,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_proxy_scope_for_skill_without_override_reuses_session_default() {
|
||||
let skill = test_skill_metadata(None);
|
||||
|
||||
assert_eq!(
|
||||
super::network_proxy_scope_for_skill(&skill),
|
||||
(NetworkProxyScope::SessionDefault, None),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_proxy_scope_for_skill_with_override_uses_skill_scope() {
|
||||
let mut skill = test_skill_metadata(None);
|
||||
skill.managed_network_override = Some(crate::skills::model::SkillManagedNetworkOverride {
|
||||
allowed_domains: Some(vec!["skill.example.com".to_string()]),
|
||||
denied_domains: Some(vec!["blocked.skill.example.com".to_string()]),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
super::network_proxy_scope_for_skill(&skill),
|
||||
(
|
||||
NetworkProxyScope::Skill {
|
||||
path_to_skills_md: PathBuf::from("/tmp/skill/SKILL.md"),
|
||||
},
|
||||
skill.managed_network_override.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execve_prompt_rejection_uses_skill_approval_for_skill_scripts() {
|
||||
let decision_source = super::DecisionSource::SkillScript {
|
||||
@@ -650,6 +681,7 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"])
|
||||
async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions() {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
|
||||
let executor = CoreShellCommandExecutor {
|
||||
session: None,
|
||||
command: vec!["echo".to_string(), "ok".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
@@ -702,6 +734,7 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions
|
||||
async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
|
||||
let executor = CoreShellCommandExecutor {
|
||||
session: None,
|
||||
command: vec!["echo".to_string(), "ok".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
@@ -776,6 +809,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir()).unwrap();
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let executor = CoreShellCommandExecutor {
|
||||
session: None,
|
||||
command: vec!["echo".to_string(), "ok".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
|
||||
Reference in New Issue
Block a user