mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Merge branch 'main' into etraut/stream-lifecycle-diagnostics
This commit is contained in:
Generated
+8
-4
@@ -2286,15 +2286,20 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"codex-app-server-protocol",
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-features",
|
||||
"codex-git-utils",
|
||||
"codex-model-provider-info",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-path",
|
||||
"core-foundation 0.9.4",
|
||||
"dns-lookup",
|
||||
"dunce",
|
||||
"futures",
|
||||
"gethostname",
|
||||
"libc",
|
||||
@@ -2318,6 +2323,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"wildmatch",
|
||||
"winapi-util",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2398,7 +2404,6 @@ dependencies = [
|
||||
"codex-utils-string",
|
||||
"codex-utils-template",
|
||||
"codex-windows-sandbox",
|
||||
"core-foundation 0.9.4",
|
||||
"core_test_support",
|
||||
"csv",
|
||||
"ctor 0.6.3",
|
||||
@@ -2449,7 +2454,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
"which 8.0.0",
|
||||
"whoami",
|
||||
"windows-sys 0.52.0",
|
||||
"wiremock",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
@@ -2559,6 +2563,7 @@ dependencies = [
|
||||
"codex-apply-patch",
|
||||
"codex-arg0",
|
||||
"codex-cloud-requirements",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-feedback",
|
||||
"codex-git-utils",
|
||||
@@ -2602,7 +2607,6 @@ dependencies = [
|
||||
"bytes",
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-config",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-test-binary-support",
|
||||
@@ -2780,7 +2784,6 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"clap",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
@@ -3117,6 +3120,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -161,7 +161,7 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) -
|
||||
}
|
||||
|
||||
fn sample_permission_profile() -> AppServerPermissionProfile {
|
||||
CorePermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::DangerFullAccess).into()
|
||||
CorePermissionProfile::Disabled.into()
|
||||
}
|
||||
|
||||
fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata {
|
||||
@@ -315,7 +315,10 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact {
|
||||
session_source: SessionSource::Exec,
|
||||
model: "gpt-5".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: CorePermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
permission_profile_cwd: PathBuf::from("/tmp"),
|
||||
reasoning_effort: None,
|
||||
reasoning_summary: None,
|
||||
service_tier: None,
|
||||
|
||||
@@ -13,12 +13,12 @@ use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use codex_protocol::protocol::HookRunStatus;
|
||||
use codex_protocol::protocol::HookSource;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -62,7 +62,8 @@ pub struct TurnResolvedConfigFact {
|
||||
pub session_source: SessionSource,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub permission_profile_cwd: PathBuf,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub reasoning_summary: Option<ReasoningSummary>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
|
||||
@@ -61,7 +61,7 @@ use codex_login::default_client::originator;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
@@ -884,7 +884,8 @@ fn codex_turn_event_params(
|
||||
session_source: _session_source,
|
||||
model,
|
||||
model_provider,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
permission_profile_cwd,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
service_tier,
|
||||
@@ -909,7 +910,10 @@ fn codex_turn_event_params(
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
model: Some(model),
|
||||
model_provider,
|
||||
sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)),
|
||||
sandbox_policy: Some(sandbox_policy_mode(
|
||||
&permission_profile,
|
||||
permission_profile_cwd.as_path(),
|
||||
)),
|
||||
reasoning_effort: reasoning_effort.map(|value| value.to_string()),
|
||||
reasoning_summary: reasoning_summary_mode(reasoning_summary),
|
||||
service_tier: service_tier
|
||||
@@ -954,12 +958,27 @@ fn codex_turn_event_params(
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_policy_mode(sandbox_policy: &SandboxPolicy) -> &'static str {
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => "full_access",
|
||||
SandboxPolicy::ReadOnly { .. } => "read_only",
|
||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace_write",
|
||||
SandboxPolicy::ExternalSandbox { .. } => "external_sandbox",
|
||||
fn sandbox_policy_mode(permission_profile: &PermissionProfile, cwd: &Path) -> &'static str {
|
||||
match permission_profile {
|
||||
PermissionProfile::Disabled => "full_access",
|
||||
PermissionProfile::External { .. } => "external_sandbox",
|
||||
PermissionProfile::Managed { .. } => {
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
if file_system_policy.has_full_disk_write_access() {
|
||||
if permission_profile.network_sandbox_policy().is_enabled() {
|
||||
"full_access"
|
||||
} else {
|
||||
"external_sandbox"
|
||||
}
|
||||
} else if file_system_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.is_empty()
|
||||
{
|
||||
"read_only"
|
||||
} else {
|
||||
"workspace_write"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,3 +1069,25 @@ pub(crate) fn normalize_path_for_skill_id(
|
||||
_ => resolved_path.to_string_lossy().replace('\\', "/"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
|
||||
#[test]
|
||||
fn managed_full_disk_with_restricted_network_reports_external_sandbox() {
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::Managed,
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sandbox_policy_mode(&permission_profile, Path::new("/")),
|
||||
"external_sandbox"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,12 @@ use codex_app_server_protocol::Result as JsonRpcResult;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
pub use codex_exec_server::EnvironmentManager;
|
||||
pub use codex_exec_server::EnvironmentManagerArgs;
|
||||
pub use codex_exec_server::ExecServerRuntimePaths;
|
||||
|
||||
+1
-7
@@ -2292,12 +2292,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -6130,4 +6124,4 @@
|
||||
}
|
||||
],
|
||||
"title": "ClientRequest"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -12739,12 +12739,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -17758,4 +17752,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocol",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -9413,12 +9413,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -15643,4 +15637,4 @@
|
||||
},
|
||||
"title": "CodexAppServerProtocolV2",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -345,12 +345,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -928,4 +922,4 @@
|
||||
],
|
||||
"title": "RawResponseItemCompletedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,12 +756,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"end_turn": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -1458,4 +1452,4 @@
|
||||
],
|
||||
"title": "ThreadResumeParams",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ReasoningItemContent } from "./ReasoningItemContent";
|
||||
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, end_turn?: boolean, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
||||
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
|
||||
/**
|
||||
* Set when using the Responses API.
|
||||
*/
|
||||
|
||||
@@ -3096,7 +3096,6 @@ mod tests {
|
||||
content: vec![codex_protocol::models::ContentItem::InputText {
|
||||
text: "plain text".into(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
|
||||
@@ -230,6 +230,9 @@ use codex_backend_client::AddCreditsNudgeCreditType as BackendAddCreditsNudgeCre
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_chatgpt::connectors;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
use codex_config::CloudRequirementsLoadError;
|
||||
use codex_config::CloudRequirementsLoadErrorCode;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::CodexThreadTurnContextOverrides;
|
||||
@@ -248,9 +251,6 @@ use codex_core::config::NetworkProxyAuditMetadata;
|
||||
use codex_core::config::ThreadStoreConfig;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config_loader::CloudRequirementsLoadError;
|
||||
use codex_core::config_loader::CloudRequirementsLoadErrorCode;
|
||||
use codex_core::config_loader::project_trust_key;
|
||||
use codex_core::exec::ExecCapturePolicy;
|
||||
use codex_core::exec::ExecExpiration;
|
||||
use codex_core::exec::ExecParams;
|
||||
@@ -2208,7 +2208,7 @@ impl CodexMessageProcessor {
|
||||
let started_network_proxy = match self.config.permissions.network.as_ref() {
|
||||
Some(spec) => match spec
|
||||
.start_proxy(
|
||||
self.config.permissions.sandbox_policy.get(),
|
||||
self.config.permissions.permission_profile.get(),
|
||||
/*policy_decider*/ None,
|
||||
/*blocked_request_observer*/ None,
|
||||
managed_network_requirements_enabled,
|
||||
@@ -2272,44 +2272,30 @@ impl CodexMessageProcessor {
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let (
|
||||
effective_policy,
|
||||
effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
) = if let Some(permission_profile) = permission_profile {
|
||||
let effective_permission_profile = if let Some(permission_profile) = permission_profile {
|
||||
let permission_profile =
|
||||
codex_protocol::models::PermissionProfile::from(permission_profile);
|
||||
let sandbox_policy = match permission_profile.to_legacy_sandbox_policy(&sandbox_cwd) {
|
||||
Ok(sandbox_policy) => sandbox_policy,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid permission profile: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let configured_file_system_sandbox_policy =
|
||||
self.config.permissions.file_system_sandbox_policy();
|
||||
Self::preserve_configured_deny_read_restrictions(
|
||||
&mut file_system_sandbox_policy,
|
||||
&configured_file_system_sandbox_policy,
|
||||
);
|
||||
let effective_permission_profile =
|
||||
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
match self
|
||||
.config
|
||||
.permissions
|
||||
.sandbox_policy
|
||||
.can_set(&sandbox_policy)
|
||||
.permission_profile
|
||||
.can_set(&effective_permission_profile)
|
||||
{
|
||||
Ok(()) => {
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
Self::preserve_configured_deny_read_restrictions(
|
||||
&mut file_system_sandbox_policy,
|
||||
&self.config.permissions.file_system_sandbox_policy,
|
||||
);
|
||||
(
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
}
|
||||
Ok(()) => effective_permission_profile,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
@@ -2321,13 +2307,39 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
} else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) {
|
||||
match self.config.permissions.sandbox_policy.can_set(&policy) {
|
||||
match self
|
||||
.config
|
||||
.permissions
|
||||
.can_set_legacy_sandbox_policy(&policy, &sandbox_cwd)
|
||||
{
|
||||
Ok(()) => {
|
||||
let file_system_sandbox_policy =
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd);
|
||||
let network_sandbox_policy =
|
||||
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
|
||||
(policy, file_system_sandbox_policy, network_sandbox_policy)
|
||||
let permission_profile =
|
||||
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
|
||||
&policy,
|
||||
),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
if let Err(err) = self
|
||||
.config
|
||||
.permissions
|
||||
.permission_profile
|
||||
.can_set(&permission_profile)
|
||||
{
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid sandbox policy: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
permission_profile
|
||||
}
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
@@ -2340,11 +2352,7 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(
|
||||
self.config.permissions.sandbox_policy.get().clone(),
|
||||
self.config.permissions.file_system_sandbox_policy.clone(),
|
||||
self.config.permissions.network_sandbox_policy,
|
||||
)
|
||||
self.config.permissions.permission_profile()
|
||||
};
|
||||
|
||||
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
|
||||
@@ -2363,9 +2371,7 @@ impl CodexMessageProcessor {
|
||||
|
||||
match codex_core::exec::build_exec_request(
|
||||
exec_params,
|
||||
&effective_policy,
|
||||
&effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
&effective_permission_profile,
|
||||
&sandbox_cwd,
|
||||
&codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
@@ -2652,16 +2658,14 @@ impl CodexMessageProcessor {
|
||||
// should still be considered "trusted" in this case.
|
||||
let requested_permissions_trust_project =
|
||||
requested_permissions_trust_project(&typesafe_overrides, config.cwd.as_path());
|
||||
let effective_permissions_trust_project = permission_profile_trusts_project(
|
||||
&config.permissions.permission_profile(),
|
||||
config.cwd.as_path(),
|
||||
);
|
||||
|
||||
if requested_cwd.is_some()
|
||||
&& config.active_project.trust_level.is_none()
|
||||
&& (requested_permissions_trust_project
|
||||
|| matches!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
|
||||
| codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
||||
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
|
||||
))
|
||||
&& (requested_permissions_trust_project || effective_permissions_trust_project)
|
||||
{
|
||||
let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd)
|
||||
.await
|
||||
@@ -8705,7 +8709,9 @@ impl CodexMessageProcessor {
|
||||
Ok(config) => {
|
||||
let setup_request = WindowsSandboxSetupRequest {
|
||||
mode,
|
||||
policy: config.permissions.sandbox_policy.get().clone(),
|
||||
policy: config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(config.cwd.as_path()),
|
||||
policy_cwd: config.cwd.to_path_buf(),
|
||||
command_cwd,
|
||||
env_map: std::env::vars().collect(),
|
||||
@@ -10160,18 +10166,20 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path)
|
||||
overrides
|
||||
.permission_profile
|
||||
.as_ref()
|
||||
.is_some_and(|profile| {
|
||||
profile
|
||||
.to_legacy_sandbox_policy(cwd)
|
||||
.is_ok_and(|sandbox_policy| {
|
||||
matches!(
|
||||
sandbox_policy,
|
||||
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
|
||||
| codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
||||
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
|
||||
)
|
||||
})
|
||||
})
|
||||
.is_some_and(|profile| permission_profile_trusts_project(profile, cwd))
|
||||
}
|
||||
|
||||
fn permission_profile_trusts_project(
|
||||
profile: &codex_protocol::models::PermissionProfile,
|
||||
cwd: &Path,
|
||||
) -> bool {
|
||||
match profile {
|
||||
codex_protocol::models::PermissionProfile::Disabled
|
||||
| codex_protocol::models::PermissionProfile::External { .. } => true,
|
||||
codex_protocol::models::PermissionProfile::Managed { .. } => profile
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(cwd, cwd),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
@@ -10456,11 +10464,11 @@ mod tests {
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::SessionThreadConfig;
|
||||
use codex_config::StaticThreadConfigLoader;
|
||||
use codex_config::ThreadConfigSource;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -10468,6 +10476,7 @@ mod tests {
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -10672,16 +10681,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn thread_response_permission_profile_preserves_enforcement() {
|
||||
let full_access_profile =
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
);
|
||||
let external_profile =
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
|
||||
},
|
||||
);
|
||||
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
|
||||
let external_profile = codex_protocol::models::PermissionProfile::External {
|
||||
network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
thread_response_permission_profile(external_profile.clone()),
|
||||
@@ -10696,17 +10699,24 @@ mod tests {
|
||||
#[test]
|
||||
fn requested_permissions_trust_project_uses_permission_profile_intent() {
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
let full_access_profile =
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
);
|
||||
let workspace_write_profile =
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
);
|
||||
let read_only_profile =
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
|
||||
let workspace_write_profile = codex_protocol::models::PermissionProfile::workspace_write();
|
||||
let read_only_profile = codex_protocol::models::PermissionProfile::read_only();
|
||||
let split_write_profile =
|
||||
codex_protocol::models::PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: cwd.clone() },
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "/tmp/project/**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
assert!(requested_permissions_trust_project(
|
||||
@@ -10723,6 +10733,13 @@ mod tests {
|
||||
},
|
||||
cwd.as_path()
|
||||
));
|
||||
assert!(requested_permissions_trust_project(
|
||||
&ConfigOverrides {
|
||||
permission_profile: Some(split_write_profile),
|
||||
..Default::default()
|
||||
},
|
||||
cwd.as_path()
|
||||
));
|
||||
assert!(!requested_permissions_trust_project(
|
||||
&ConfigOverrides {
|
||||
permission_profile: Some(read_only_profile),
|
||||
@@ -10915,10 +10932,7 @@ mod tests {
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
permission_profile:
|
||||
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
|
||||
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
|
||||
),
|
||||
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
|
||||
cwd,
|
||||
ephemeral: false,
|
||||
reasoning_effort: None,
|
||||
|
||||
@@ -34,9 +34,9 @@ use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_PARAMS_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_params;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
@@ -158,7 +158,7 @@ impl CommandExecManager {
|
||||
} = params;
|
||||
if process_id.is_none() && (tty || stream_stdin || stream_stdout_stderr) {
|
||||
return Err(invalid_request(
|
||||
"command/exec tty or streaming requires a client-supplied processId".to_string(),
|
||||
"command/exec tty or streaming requires a client-supplied processId",
|
||||
));
|
||||
}
|
||||
let process_id = process_id.map_or_else(
|
||||
@@ -178,12 +178,12 @@ impl CommandExecManager {
|
||||
if matches!(exec_request.sandbox, SandboxType::WindowsRestrictedToken) {
|
||||
if tty || stream_stdin || stream_stdout_stderr {
|
||||
return Err(invalid_request(
|
||||
"streaming command/exec is not supported with windows sandbox".to_string(),
|
||||
"streaming command/exec is not supported with windows sandbox",
|
||||
));
|
||||
}
|
||||
if output_bytes_cap != Some(DEFAULT_OUTPUT_BYTES_CAP) {
|
||||
return Err(invalid_request(
|
||||
"custom outputBytesCap is not supported with windows sandbox".to_string(),
|
||||
"custom outputBytesCap is not supported with windows sandbox",
|
||||
));
|
||||
}
|
||||
if let InternalProcessId::Client(_) = &process_id {
|
||||
@@ -249,7 +249,7 @@ impl CommandExecManager {
|
||||
let sessions = Arc::clone(&self.sessions);
|
||||
let (program, args) = command
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_request("command must not be empty".to_string()))?;
|
||||
.ok_or_else(|| invalid_request("command must not be empty"))?;
|
||||
{
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
if sessions.contains_key(&process_key) {
|
||||
@@ -312,7 +312,7 @@ impl CommandExecManager {
|
||||
) -> Result<CommandExecWriteResponse, JSONRPCErrorError> {
|
||||
if params.delta_base64.is_none() && !params.close_stdin {
|
||||
return Err(invalid_params(
|
||||
"command/exec/write requires deltaBase64 or closeStdin".to_string(),
|
||||
"command/exec/write requires deltaBase64 or closeStdin",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ impl CommandExecManager {
|
||||
};
|
||||
let CommandExecSession::Active { control_tx } = session else {
|
||||
return Err(invalid_request(
|
||||
"command/exec/write, command/exec/terminate, and command/exec/resize are not supported for windows sandbox processes".to_string(),
|
||||
"command/exec/write, command/exec/terminate, and command/exec/resize are not supported for windows sandbox processes",
|
||||
));
|
||||
};
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
@@ -635,7 +635,7 @@ async fn handle_process_write(
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if !stream_stdin {
|
||||
return Err(invalid_request(
|
||||
"stdin streaming is not enabled for this command/exec".to_string(),
|
||||
"stdin streaming is not enabled for this command/exec",
|
||||
));
|
||||
}
|
||||
if !delta.is_empty() {
|
||||
@@ -643,7 +643,7 @@ async fn handle_process_write(
|
||||
.writer_sender()
|
||||
.send(delta)
|
||||
.await
|
||||
.map_err(|_| invalid_request("stdin is already closed".to_string()))?;
|
||||
.map_err(|_| invalid_request("stdin is already closed"))?;
|
||||
}
|
||||
if close_stdin {
|
||||
session.close_stdin();
|
||||
@@ -665,7 +665,7 @@ pub(crate) fn terminal_size_from_protocol(
|
||||
) -> Result<TerminalSize, JSONRPCErrorError> {
|
||||
if size.rows == 0 || size.cols == 0 {
|
||||
return Err(invalid_params(
|
||||
"command/exec size rows and cols must be greater than 0".to_string(),
|
||||
"command/exec size rows and cols must be greater than 0",
|
||||
));
|
||||
}
|
||||
Ok(TerminalSize {
|
||||
@@ -681,38 +681,13 @@ fn command_no_longer_running_error(process_id: &InternalProcessId) -> JSONRPCErr
|
||||
))
|
||||
}
|
||||
|
||||
fn invalid_request(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_params(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_PARAMS_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_error(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -729,12 +704,10 @@ mod tests {
|
||||
use crate::outgoing_message::OutgoingMessage;
|
||||
|
||||
fn windows_sandbox_exec_request() -> ExecRequest {
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
};
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
ExecRequest::new(
|
||||
vec!["cmd".to_string()],
|
||||
AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
cwd,
|
||||
HashMap::new(),
|
||||
/*network*/ None,
|
||||
ExecExpiration::DefaultTimeout,
|
||||
@@ -742,9 +715,7 @@ mod tests {
|
||||
SandboxType::WindowsRestrictedToken,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*windows_sandbox_private_desktop*/ false,
|
||||
sandbox_policy.clone(),
|
||||
FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
PermissionProfile::read_only(),
|
||||
/*arg0*/ None,
|
||||
)
|
||||
}
|
||||
@@ -834,9 +805,7 @@ mod tests {
|
||||
connection_id: ConnectionId(8),
|
||||
request_id: codex_app_server_protocol::RequestId::Integer(100),
|
||||
};
|
||||
let sandbox_policy = SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
};
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
|
||||
manager
|
||||
.start(StartCommandExecParams {
|
||||
@@ -845,7 +814,7 @@ mod tests {
|
||||
process_id: Some("proc-100".to_string()),
|
||||
exec_request: ExecRequest::new(
|
||||
vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()],
|
||||
AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
cwd.clone(),
|
||||
HashMap::new(),
|
||||
/*network*/ None,
|
||||
ExecExpiration::Cancellation(CancellationToken::new()),
|
||||
@@ -853,9 +822,7 @@ mod tests {
|
||||
SandboxType::None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
/*windows_sandbox_private_desktop*/ false,
|
||||
sandbox_policy.clone(),
|
||||
FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::from(&sandbox_policy),
|
||||
PermissionProfile::read_only(),
|
||||
/*arg0*/ None,
|
||||
),
|
||||
started_network_proxy: None,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::config_manager_service::ConfigManagerError;
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use async_trait::async_trait;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
@@ -22,15 +23,15 @@ use codex_app_server_protocol::NetworkDomainPermission;
|
||||
use codex_app_server_protocol::NetworkRequirements;
|
||||
use codex_app_server_protocol::NetworkUnixSocketPermission;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_config::HookHandlerConfig as CoreHookHandlerConfig;
|
||||
use codex_config::ManagedHooksRequirementsToml;
|
||||
use codex_config::MatcherGroup as CoreMatcherGroup;
|
||||
use codex_config::ResidencyRequirement as CoreResidencyRequirement;
|
||||
use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::ConfigRequirementsToml;
|
||||
use codex_core::config_loader::HookEventsToml;
|
||||
use codex_core::config_loader::HookHandlerConfig as CoreHookHandlerConfig;
|
||||
use codex_core::config_loader::ManagedHooksRequirementsToml;
|
||||
use codex_core::config_loader::MatcherGroup as CoreMatcherGroup;
|
||||
use codex_core::config_loader::ResidencyRequirement as CoreResidencyRequirement;
|
||||
use codex_core::config_loader::SandboxModeRequirement as CoreSandboxModeRequirement;
|
||||
use codex_core::plugins::PluginId;
|
||||
use codex_core_plugins::loader::installed_plugin_telemetry_metadata;
|
||||
use codex_core_plugins::toggles::collect_plugin_enabled_candidates;
|
||||
@@ -99,10 +100,10 @@ impl ConfigApi {
|
||||
self.config_manager
|
||||
.load_latest_config(fallback_cwd)
|
||||
.await
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to resolve feature override precedence: {err}"),
|
||||
data: None,
|
||||
.map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to resolve feature override precedence: {err}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,14 +198,10 @@ impl ConfigApi {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"unsupported feature enablement `{key}`: currently supported features are {}",
|
||||
SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT.join(", ")
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(format!(
|
||||
"unsupported feature enablement `{key}`: currently supported features are {}",
|
||||
SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
let message = if let Some(feature) = feature_for_key(key) {
|
||||
@@ -215,11 +212,7 @@ impl ConfigApi {
|
||||
} else {
|
||||
format!("invalid feature enablement `{key}`")
|
||||
};
|
||||
return Err(JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data: None,
|
||||
});
|
||||
return Err(invalid_request(message));
|
||||
}
|
||||
|
||||
if enablement.is_empty() {
|
||||
@@ -232,11 +225,7 @@ impl ConfigApi {
|
||||
.iter()
|
||||
.map(|(name, enabled)| (name.clone(), *enabled)),
|
||||
)
|
||||
.map_err(|_| JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "failed to update feature enablement".to_string(),
|
||||
data: None,
|
||||
})?;
|
||||
.map_err(|_| internal_error("failed to update feature enablement"))?;
|
||||
|
||||
self.load_latest_config(/*fallback_cwd*/ None).await?;
|
||||
self.user_config_reloader.reload_user_config().await;
|
||||
@@ -388,20 +377,20 @@ fn map_residency_requirement_to_api(
|
||||
}
|
||||
|
||||
fn map_network_requirements_to_api(
|
||||
network: codex_core::config_loader::NetworkRequirementsToml,
|
||||
network: codex_config::NetworkRequirementsToml,
|
||||
) -> NetworkRequirements {
|
||||
let allowed_domains = network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_core::config_loader::NetworkDomainPermissionsToml::allowed_domains);
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains);
|
||||
let denied_domains = network
|
||||
.domains
|
||||
.as_ref()
|
||||
.and_then(codex_core::config_loader::NetworkDomainPermissionsToml::denied_domains);
|
||||
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains);
|
||||
let allow_unix_sockets = network
|
||||
.unix_sockets
|
||||
.as_ref()
|
||||
.map(codex_core::config_loader::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
||||
.map(codex_config::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
||||
.filter(|entries| !entries.is_empty());
|
||||
|
||||
NetworkRequirements {
|
||||
@@ -438,28 +427,20 @@ fn map_network_requirements_to_api(
|
||||
}
|
||||
|
||||
fn map_network_domain_permission_to_api(
|
||||
permission: codex_core::config_loader::NetworkDomainPermissionToml,
|
||||
permission: codex_config::NetworkDomainPermissionToml,
|
||||
) -> NetworkDomainPermission {
|
||||
match permission {
|
||||
codex_core::config_loader::NetworkDomainPermissionToml::Allow => {
|
||||
NetworkDomainPermission::Allow
|
||||
}
|
||||
codex_core::config_loader::NetworkDomainPermissionToml::Deny => {
|
||||
NetworkDomainPermission::Deny
|
||||
}
|
||||
codex_config::NetworkDomainPermissionToml::Allow => NetworkDomainPermission::Allow,
|
||||
codex_config::NetworkDomainPermissionToml::Deny => NetworkDomainPermission::Deny,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_network_unix_socket_permission_to_api(
|
||||
permission: codex_core::config_loader::NetworkUnixSocketPermissionToml,
|
||||
permission: codex_config::NetworkUnixSocketPermissionToml,
|
||||
) -> NetworkUnixSocketPermission {
|
||||
match permission {
|
||||
codex_core::config_loader::NetworkUnixSocketPermissionToml::Allow => {
|
||||
NetworkUnixSocketPermission::Allow
|
||||
}
|
||||
codex_core::config_loader::NetworkUnixSocketPermissionToml::None => {
|
||||
NetworkUnixSocketPermission::None
|
||||
}
|
||||
codex_config::NetworkUnixSocketPermissionToml::Allow => NetworkUnixSocketPermission::Allow,
|
||||
codex_config::NetworkUnixSocketPermissionToml::None => NetworkUnixSocketPermission::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,11 +449,7 @@ fn map_error(err: ConfigManagerError) -> JSONRPCErrorError {
|
||||
return config_write_error(code, err.to_string());
|
||||
}
|
||||
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
}
|
||||
internal_error(err.to_string())
|
||||
}
|
||||
|
||||
fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) -> JSONRPCErrorError {
|
||||
@@ -491,13 +468,13 @@ mod tests {
|
||||
use crate::config_manager::apply_runtime_feature_enablement;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::config_loader::NetworkDomainPermissionToml as CoreNetworkDomainPermissionToml;
|
||||
use codex_core::config_loader::NetworkDomainPermissionsToml as CoreNetworkDomainPermissionsToml;
|
||||
use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionToml as CoreNetworkUnixSocketPermissionToml;
|
||||
use codex_core::config_loader::NetworkUnixSocketPermissionsToml as CoreNetworkUnixSocketPermissionsToml;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NetworkDomainPermissionToml as CoreNetworkDomainPermissionToml;
|
||||
use codex_config::NetworkDomainPermissionsToml as CoreNetworkDomainPermissionsToml;
|
||||
use codex_config::NetworkRequirementsToml as CoreNetworkRequirementsToml;
|
||||
use codex_config::NetworkUnixSocketPermissionToml as CoreNetworkUnixSocketPermissionToml;
|
||||
use codex_config::NetworkUnixSocketPermissionsToml as CoreNetworkUnixSocketPermissionsToml;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -539,11 +516,9 @@ mod tests {
|
||||
CoreSandboxModeRequirement::ExternalSandbox,
|
||||
]),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![
|
||||
codex_core::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
guardian_policy_config: None,
|
||||
feature_requirements: Some(codex_core::config_loader::FeatureRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: std::collections::BTreeMap::from([
|
||||
("apps".to_string(), false),
|
||||
("personality".to_string(), true),
|
||||
@@ -809,11 +784,9 @@ mod tests {
|
||||
)])
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(ConfigRequirementsToml {
|
||||
feature_requirements: Some(
|
||||
codex_core::config_loader::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("apps".to_string(), false)]),
|
||||
},
|
||||
),
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("apps".to_string(), false)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_cloud_requirements::cloud_requirements_loader;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_core::config_loader::load_config_layers_state;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::feature_for_key;
|
||||
use codex_login::AuthManager;
|
||||
@@ -33,7 +33,6 @@ pub(crate) struct ConfigManager {
|
||||
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
||||
host_name: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
@@ -44,27 +43,6 @@ impl ConfigManager {
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
) -> Self {
|
||||
Self::new_with_host_name(
|
||||
codex_home,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
arg0_paths,
|
||||
thread_config_loader,
|
||||
codex_config::host_name(),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_with_host_name(
|
||||
codex_home: PathBuf,
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
thread_config_loader: Arc<dyn ThreadConfigLoader>,
|
||||
host_name: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
@@ -74,7 +52,6 @@ impl ConfigManager {
|
||||
cloud_requirements: Arc::new(RwLock::new(cloud_requirements)),
|
||||
arg0_paths,
|
||||
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
|
||||
host_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +206,6 @@ impl ConfigManager {
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_requirements(self.current_cloud_requirements())
|
||||
.thread_config_loader(self.current_thread_config_loader())
|
||||
.host_name(self.host_name.clone())
|
||||
.build()
|
||||
.await?;
|
||||
self.apply_runtime_feature_enablement(&mut config);
|
||||
@@ -257,7 +233,6 @@ impl ConfigManager {
|
||||
self.loader_overrides.clone(),
|
||||
self.current_cloud_requirements(),
|
||||
thread_config_loader.as_ref(),
|
||||
self.host_name.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -285,16 +260,14 @@ impl ConfigManager {
|
||||
cli_overrides: Vec<(String, TomlValue)>,
|
||||
loader_overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
host_name: Option<String>,
|
||||
) -> Self {
|
||||
Self::new_with_host_name(
|
||||
Self::new(
|
||||
codex_home,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
Arg0DispatchPaths::default(),
|
||||
Arc::new(codex_config::NoopThreadConfigLoader),
|
||||
host_name,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -305,7 +278,6 @@ impl ConfigManager {
|
||||
Vec::new(),
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::OverriddenMetadata;
|
||||
use codex_app_server_protocol::WriteStatus;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::merge_toml_values;
|
||||
use codex_core::config::deserialize_config_toml_with_base;
|
||||
use codex_core::config::edit::ConfigEdit;
|
||||
use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::validate_feature_requirements_for_config_toml;
|
||||
use codex_core::config_loader::ConfigLayerEntry;
|
||||
use codex_core::config_loader::ConfigLayerStack;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::ConfigRequirementsToml;
|
||||
use codex_core::config_loader::merge_toml_values;
|
||||
use codex_core::path_utils;
|
||||
use codex_core::path_utils::SymlinkWritePaths;
|
||||
use codex_core::path_utils::resolve_symlink_write_paths;
|
||||
|
||||
@@ -4,9 +4,9 @@ use codex_app_server_protocol::AppConfig;
|
||||
use codex_app_server_protocol::AppToolApproval;
|
||||
use codex_app_server_protocol::AppsConfig;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::FeatureRequirementsToml;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -226,7 +226,6 @@ async fn read_includes_origins_and_layers() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let response = service
|
||||
@@ -305,7 +304,6 @@ writable_roots = ["~/code"]
|
||||
vec![],
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let response = service
|
||||
@@ -346,7 +344,6 @@ async fn write_value_reports_override() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let result = service
|
||||
@@ -446,7 +443,6 @@ async fn invalid_user_value_rejected_even_if_overridden_by_managed() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let error = service
|
||||
@@ -514,7 +510,6 @@ async fn write_value_rejects_feature_requirement_conflict() {
|
||||
..Default::default()
|
||||
}))
|
||||
}),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let error = service
|
||||
@@ -561,7 +556,6 @@ async fn write_value_rejects_profile_feature_requirement_conflict() {
|
||||
..Default::default()
|
||||
}))
|
||||
}),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let error = service
|
||||
@@ -612,7 +606,6 @@ async fn read_reports_managed_overrides_user_and_session_flags() {
|
||||
cli_overrides,
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let response = service
|
||||
@@ -666,7 +659,6 @@ async fn write_value_reports_managed_override() {
|
||||
vec![],
|
||||
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
|
||||
CloudRequirementsLoader::default(),
|
||||
/*host_name*/ None,
|
||||
);
|
||||
|
||||
let result = service
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
@@ -302,16 +302,13 @@ fn protection_class_from_store(
|
||||
}
|
||||
|
||||
fn map_device_key_error(error: DeviceKeyError) -> JSONRPCErrorError {
|
||||
let code = match error {
|
||||
match &error {
|
||||
DeviceKeyError::DegradedProtectionNotAllowed { .. }
|
||||
| DeviceKeyError::HardwareBackedKeysUnavailable
|
||||
| DeviceKeyError::KeyNotFound
|
||||
| DeviceKeyError::InvalidPayload(_) => INVALID_REQUEST_ERROR_CODE,
|
||||
DeviceKeyError::Platform(_) | DeviceKeyError::Crypto(_) => INTERNAL_ERROR_CODE,
|
||||
};
|
||||
JSONRPCErrorError {
|
||||
code,
|
||||
message: error.to_string(),
|
||||
data: None,
|
||||
| DeviceKeyError::InvalidPayload(_) => invalid_request(error.to_string()),
|
||||
DeviceKeyError::Platform(_) | DeviceKeyError::Crypto(_) => {
|
||||
internal_error(error.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
pub(crate) const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||
pub const INVALID_PARAMS_ERROR_CODE: i64 = -32602;
|
||||
pub(crate) const INTERNAL_ERROR_CODE: i64 = -32603;
|
||||
pub(crate) const OVERLOADED_ERROR_CODE: i64 = -32001;
|
||||
pub const INPUT_TOO_LARGE_ERROR_CODE: &str = "input_too_large";
|
||||
|
||||
pub(crate) fn invalid_request(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
error(INVALID_REQUEST_ERROR_CODE, message)
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_params(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
error(INVALID_PARAMS_ERROR_CODE, message)
|
||||
}
|
||||
|
||||
pub(crate) fn internal_error(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
error(INTERNAL_ERROR_CODE, message)
|
||||
}
|
||||
|
||||
fn error(code: i64, message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::config::external_agent_config::ExternalAgentConfigMigrationItem as Co
|
||||
use crate::config::external_agent_config::ExternalAgentConfigMigrationItemType as CoreMigrationItemType;
|
||||
use crate::config::external_agent_config::ExternalAgentConfigService;
|
||||
use crate::config::external_agent_config::PendingPluginImport;
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
|
||||
use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportParams;
|
||||
@@ -12,7 +12,6 @@ use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::MigrationDetails;
|
||||
use codex_app_server_protocol::PluginsMigration;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -38,7 +37,7 @@ impl ExternalAgentConfigApi {
|
||||
cwds: params.cwds,
|
||||
})
|
||||
.await
|
||||
.map_err(map_io_error)?;
|
||||
.map_err(|err| internal_error(err.to_string()))?;
|
||||
|
||||
Ok(ExternalAgentConfigDetectResponse {
|
||||
items: items
|
||||
@@ -125,7 +124,7 @@ impl ExternalAgentConfigApi {
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_io_error)
|
||||
.map_err(|err| internal_error(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn complete_pending_plugin_import(
|
||||
@@ -139,14 +138,6 @@ impl ExternalAgentConfigApi {
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(map_io_error)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_io_error(err: io::Error) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
.map_err(|err| internal_error(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_app_server_protocol::FsCopyParams;
|
||||
@@ -158,22 +158,10 @@ impl FsApi {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_request(message: impl Into<String>) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
|
||||
if err.kind() == io::ErrorKind::InvalidInput {
|
||||
invalid_request(err.to_string())
|
||||
} else {
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: err.to_string(),
|
||||
data: None,
|
||||
}
|
||||
internal_error(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::fs_api::invalid_request;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use codex_app_server_protocol::FsChangedNotification;
|
||||
|
||||
@@ -77,10 +77,10 @@ use codex_app_server_protocol::Result;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::AuthManager;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_exec_server::EnvironmentManagerArgs;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
@@ -42,11 +42,11 @@ use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::TextPosition as AppTextPosition;
|
||||
use codex_app_server_protocol::TextRange as AppTextRange;
|
||||
use codex_config::ConfigLoadError;
|
||||
use codex_config::TextRange as CoreTextRange;
|
||||
use codex_core::ExecPolicyError;
|
||||
use codex_core::check_execpolicy_for_warnings;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_core::config_loader::ConfigLoadError;
|
||||
use codex_core::config_loader::TextRange as CoreTextRange;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_feedback::CodexFeedback;
|
||||
@@ -518,7 +518,7 @@ pub async fn run_main_with_transport_options(
|
||||
});
|
||||
}
|
||||
if let Some(warning) =
|
||||
codex_core::config::system_bwrap_warning(config.permissions.sandbox_policy.get())
|
||||
codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get())
|
||||
{
|
||||
config_warnings.push(ConfigWarningNotification {
|
||||
summary: warning,
|
||||
|
||||
@@ -6,7 +6,7 @@ use codex_app_server::PluginStartupTasks;
|
||||
use codex_app_server::run_main_with_transport_options;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_arg0::arg0_dispatch_or_else;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::codex_message_processor::CodexMessageProcessorArgs;
|
||||
use crate::config_api::ConfigApi;
|
||||
use crate::config_manager::ConfigManager;
|
||||
use crate::device_key_api::DeviceKeyApi;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::external_agent_config_api::ExternalAgentConfigApi;
|
||||
use crate::fs_api::FsApi;
|
||||
use crate::fs_watch::FsWatchManager;
|
||||
@@ -34,7 +34,6 @@ use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigReadParams;
|
||||
use codex_app_server_protocol::ConfigValueWriteParams;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::DeviceKeyCreateParams;
|
||||
@@ -42,20 +41,10 @@ use codex_app_server_protocol::DeviceKeyPublicParams;
|
||||
use codex_app_server_protocol::DeviceKeySignParams;
|
||||
use codex_app_server_protocol::ExperimentalApi;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams;
|
||||
use codex_app_server_protocol::ExternalAgentConfigDetectParams;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportParams;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
||||
use codex_app_server_protocol::FsCopyParams;
|
||||
use codex_app_server_protocol::FsCreateDirectoryParams;
|
||||
use codex_app_server_protocol::FsGetMetadataParams;
|
||||
use codex_app_server_protocol::FsReadDirectoryParams;
|
||||
use codex_app_server_protocol::FsReadFileParams;
|
||||
use codex_app_server_protocol::FsRemoveParams;
|
||||
use codex_app_server_protocol::FsUnwatchParams;
|
||||
use codex_app_server_protocol::FsWatchParams;
|
||||
use codex_app_server_protocol::FsWriteFileParams;
|
||||
use codex_app_server_protocol::InitializeResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
@@ -390,43 +379,28 @@ impl MessageProcessor {
|
||||
Arc::clone(&self.outgoing),
|
||||
request_context.clone(),
|
||||
async {
|
||||
let request_json = match serde_json::to_value(&request) {
|
||||
Ok(request_json) => request_json,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("Invalid request: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id.clone(), error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let codex_request = match serde_json::from_value::<ClientRequest>(request_json) {
|
||||
Ok(codex_request) => codex_request,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("Invalid request: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id.clone(), error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Websocket callers finalize outbound readiness in lib.rs after mirroring
|
||||
// session state into outbound state and sending initialize notifications to
|
||||
// this specific connection. Passing `None` avoids marking the connection
|
||||
// ready too early from inside the shared request handler.
|
||||
self.handle_client_request(
|
||||
request_id.clone(),
|
||||
codex_request,
|
||||
Arc::clone(&session),
|
||||
/*outbound_initialized*/ None,
|
||||
request_context.clone(),
|
||||
)
|
||||
let result = async {
|
||||
let request_json = serde_json::to_value(&request)
|
||||
.map_err(|err| invalid_request(format!("Invalid request: {err}")))?;
|
||||
let codex_request = serde_json::from_value::<ClientRequest>(request_json)
|
||||
.map_err(|err| invalid_request(format!("Invalid request: {err}")))?;
|
||||
// Websocket callers finalize outbound readiness in lib.rs after mirroring
|
||||
// session state into outbound state and sending initialize notifications to
|
||||
// this specific connection. Passing `None` avoids marking the connection
|
||||
// ready too early from inside the shared request handler.
|
||||
self.handle_client_request(
|
||||
request_id.clone(),
|
||||
codex_request,
|
||||
Arc::clone(&session),
|
||||
/*outbound_initialized*/ None,
|
||||
request_context.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
self.outgoing.send_error(request_id.clone(), error).await;
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -463,14 +437,18 @@ impl MessageProcessor {
|
||||
// In-process clients do not have the websocket transport loop that performs
|
||||
// post-initialize bookkeeping, so they still finalize outbound readiness in
|
||||
// the shared request handler.
|
||||
self.handle_client_request(
|
||||
request_id.clone(),
|
||||
request,
|
||||
Arc::clone(&session),
|
||||
Some(outbound_initialized),
|
||||
request_context.clone(),
|
||||
)
|
||||
.await;
|
||||
let result = self
|
||||
.handle_client_request(
|
||||
request_id.clone(),
|
||||
request,
|
||||
Arc::clone(&session),
|
||||
Some(outbound_initialized),
|
||||
request_context.clone(),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
self.outgoing.send_error(request_id.clone(), error).await;
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -598,7 +576,7 @@ impl MessageProcessor {
|
||||
// lib.rs can deliver connection-scoped initialize notifications first.
|
||||
outbound_initialized: Option<&AtomicBool>,
|
||||
request_context: RequestContext,
|
||||
) {
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
if let ClientRequest::Initialize { request_id, params } = codex_request {
|
||||
// Handle Initialize internally so CodexMessageProcessor does not have to concern
|
||||
@@ -608,13 +586,7 @@ impl MessageProcessor {
|
||||
request_id,
|
||||
};
|
||||
if session.initialized() {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "Already initialized".to_string(),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(connection_request_id, error).await;
|
||||
return;
|
||||
return Err(invalid_request("Already initialized"));
|
||||
}
|
||||
|
||||
// TODO(maxj): Revisit capability scoping for `experimental_api_enabled`.
|
||||
@@ -642,17 +614,9 @@ impl MessageProcessor {
|
||||
// Validate before committing; set_default_originator validates while
|
||||
// mutating process-global metadata.
|
||||
if HeaderValue::from_str(&name).is_err() {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!(
|
||||
"Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value."
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing
|
||||
.send_error(connection_request_id.clone(), error)
|
||||
.await;
|
||||
return;
|
||||
return Err(invalid_request(format!(
|
||||
"Invalid clientInfo.name: '{name}'. Must be a valid HTTP header value."
|
||||
)));
|
||||
}
|
||||
let originator = name.clone();
|
||||
let user_agent_suffix = format!("{name}; {version}");
|
||||
@@ -668,13 +632,7 @@ impl MessageProcessor {
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "Already initialized".to_string(),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(connection_request_id, error).await;
|
||||
return;
|
||||
return Err(invalid_request("Already initialized"));
|
||||
}
|
||||
|
||||
// Only the request that wins session initialization may mutate
|
||||
@@ -729,7 +687,7 @@ impl MessageProcessor {
|
||||
.connection_initialized(connection_id)
|
||||
.await;
|
||||
}
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.dispatch_initialized_client_request(
|
||||
@@ -738,7 +696,7 @@ impl MessageProcessor {
|
||||
session,
|
||||
request_context,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
}
|
||||
|
||||
async fn dispatch_initialized_client_request(
|
||||
@@ -747,27 +705,15 @@ impl MessageProcessor {
|
||||
codex_request: ClientRequest,
|
||||
session: Arc<ConnectionSessionState>,
|
||||
request_context: RequestContext,
|
||||
) {
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
if !session.initialized() {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "Not initialized".to_string(),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(connection_request_id, error).await;
|
||||
return;
|
||||
return Err(invalid_request("Not initialized"));
|
||||
}
|
||||
|
||||
if let Some(reason) = codex_request.experimental_reason()
|
||||
&& !session.experimental_api_enabled()
|
||||
{
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: experimental_required_message(reason),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(connection_request_id, error).await;
|
||||
return;
|
||||
return Err(invalid_request(experimental_required_message(reason)));
|
||||
}
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics)
|
||||
@@ -793,7 +739,7 @@ impl MessageProcessor {
|
||||
client_version,
|
||||
device_key_requests_allowed,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
}
|
||||
|
||||
async fn handle_initialized_client_request(
|
||||
@@ -804,66 +750,48 @@ impl MessageProcessor {
|
||||
app_server_client_name: Option<String>,
|
||||
client_version: Option<String>,
|
||||
device_key_requests_allowed: bool,
|
||||
) {
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let connection_id = connection_request_id.connection_id;
|
||||
let request_id_for_connection = |request_id| ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
};
|
||||
|
||||
match codex_request {
|
||||
ClientRequest::ConfigRead { request_id, params } => {
|
||||
self.handle_config_read(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.config_api.read(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExternalAgentConfigDetect { request_id, params } => {
|
||||
self.handle_external_agent_config_detect(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.external_agent_config_api.detect(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExternalAgentConfigImport { request_id, params } => {
|
||||
self.handle_external_agent_config_import(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
request_id_for_connection(request_id),
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
}
|
||||
ClientRequest::ConfigValueWrite { request_id, params } => {
|
||||
self.handle_config_value_write(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.handle_config_value_write(request_id_for_connection(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigBatchWrite { request_id, params } => {
|
||||
self.handle_config_batch_write(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.handle_config_batch_write(request_id_for_connection(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExperimentalFeatureEnablementSet { request_id, params } => {
|
||||
self.handle_experimental_feature_enablement_set(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
request_id_for_connection(request_id),
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
@@ -872,133 +800,105 @@ impl MessageProcessor {
|
||||
request_id,
|
||||
params: _,
|
||||
} => {
|
||||
self.handle_config_requirements_read(ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
})
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.config_api.config_requirements_read().await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::DeviceKeyCreate { request_id, params } => {
|
||||
self.handle_device_key_create(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
request_id_for_connection(request_id),
|
||||
params,
|
||||
device_key_requests_allowed,
|
||||
);
|
||||
}
|
||||
ClientRequest::DeviceKeyPublic { request_id, params } => {
|
||||
self.handle_device_key_public(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
request_id_for_connection(request_id),
|
||||
params,
|
||||
device_key_requests_allowed,
|
||||
);
|
||||
}
|
||||
ClientRequest::DeviceKeySign { request_id, params } => {
|
||||
self.handle_device_key_sign(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
request_id_for_connection(request_id),
|
||||
params,
|
||||
device_key_requests_allowed,
|
||||
);
|
||||
}
|
||||
ClientRequest::FsReadFile { request_id, params } => {
|
||||
self.handle_fs_read_file(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.read_file(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsWriteFile { request_id, params } => {
|
||||
self.handle_fs_write_file(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.write_file(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsCreateDirectory { request_id, params } => {
|
||||
self.handle_fs_create_directory(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.create_directory(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsGetMetadata { request_id, params } => {
|
||||
self.handle_fs_get_metadata(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.get_metadata(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsReadDirectory { request_id, params } => {
|
||||
self.handle_fs_read_directory(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.read_directory(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsRemove { request_id, params } => {
|
||||
self.handle_fs_remove(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.remove(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsCopy { request_id, params } => {
|
||||
self.handle_fs_copy(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_api.copy(params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsWatch { request_id, params } => {
|
||||
self.handle_fs_watch(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
connection_id,
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_watch_manager.watch(connection_id, params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::FsUnwatch { request_id, params } => {
|
||||
self.handle_fs_unwatch(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
connection_id,
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
self.outgoing
|
||||
.send_result(
|
||||
request_id_for_connection(request_id),
|
||||
self.fs_watch_manager.unwatch(connection_id, params).await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
other => {
|
||||
// Box the delegated future so this wrapper's async state machine does not
|
||||
@@ -1016,13 +916,7 @@ impl MessageProcessor {
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_config_read(&self, request_id: ConnectionRequestId, params: ConfigReadParams) {
|
||||
match self.config_api.read(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_config_value_write(
|
||||
@@ -1167,13 +1061,6 @@ impl MessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_config_requirements_read(&self, request_id: ConnectionRequestId) {
|
||||
match self.config_api.config_requirements_read().await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_device_key_create(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -1230,193 +1117,80 @@ impl MessageProcessor {
|
||||
let device_key_api = self.device_key_api.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
tokio::spawn(async move {
|
||||
if !device_key_requests_allowed {
|
||||
outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("{method} is not available over remote transports"),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
match run_request(device_key_api).await {
|
||||
Ok(response) => outgoing.send_response(request_id, response).await,
|
||||
Err(error) => outgoing.send_error(request_id, error).await,
|
||||
let result = async {
|
||||
if !device_key_requests_allowed {
|
||||
return Err(invalid_request(format!(
|
||||
"{method} is not available over remote transports"
|
||||
)));
|
||||
}
|
||||
run_request(device_key_api).await
|
||||
}
|
||||
.await;
|
||||
outgoing.send_result(request_id, result).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_external_agent_config_detect(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ExternalAgentConfigDetectParams,
|
||||
) {
|
||||
match self.external_agent_config_api.detect(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_external_agent_config_import(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ExternalAgentConfigImportParams,
|
||||
) {
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let has_plugin_imports = params.migration_items.iter().any(|item| {
|
||||
matches!(
|
||||
item.item_type,
|
||||
ExternalAgentConfigMigrationItemType::Plugins
|
||||
)
|
||||
});
|
||||
match self.external_agent_config_api.import(params).await {
|
||||
Ok(pending_plugin_imports) => {
|
||||
if has_plugin_imports {
|
||||
self.handle_config_mutation().await;
|
||||
}
|
||||
self.outgoing
|
||||
.send_response(request_id, ExternalAgentConfigImportResponse {})
|
||||
.await;
|
||||
|
||||
if !has_plugin_imports {
|
||||
return;
|
||||
}
|
||||
let pending_plugin_imports = self.external_agent_config_api.import(params).await?;
|
||||
if has_plugin_imports {
|
||||
self.handle_config_mutation().await;
|
||||
}
|
||||
self.outgoing
|
||||
.send_response(request_id, ExternalAgentConfigImportResponse {})
|
||||
.await;
|
||||
|
||||
if pending_plugin_imports.is_empty() {
|
||||
self.outgoing
|
||||
.send_server_notification(
|
||||
ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
ExternalAgentConfigImportCompletedNotification {},
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
if !has_plugin_imports {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let external_agent_config_api = self.external_agent_config_api.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let thread_manager = Arc::clone(&self.thread_manager);
|
||||
tokio::spawn(async move {
|
||||
for pending_plugin_import in pending_plugin_imports {
|
||||
match external_agent_config_api
|
||||
.complete_pending_plugin_import(pending_plugin_import)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
error = %error.message,
|
||||
"external agent config plugin import failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
if pending_plugin_imports.is_empty() {
|
||||
self.outgoing
|
||||
.send_server_notification(ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
ExternalAgentConfigImportCompletedNotification {},
|
||||
))
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let external_agent_config_api = self.external_agent_config_api.clone();
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let thread_manager = Arc::clone(&self.thread_manager);
|
||||
tokio::spawn(async move {
|
||||
for pending_plugin_import in pending_plugin_imports {
|
||||
match external_agent_config_api
|
||||
.complete_pending_plugin_import(pending_plugin_import)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
error = %error.message,
|
||||
"external agent config plugin import failed"
|
||||
);
|
||||
}
|
||||
thread_manager.plugins_manager().clear_cache();
|
||||
thread_manager.skills_manager().clear_cache();
|
||||
outgoing
|
||||
.send_server_notification(
|
||||
ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
ExternalAgentConfigImportCompletedNotification {},
|
||||
),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
thread_manager.plugins_manager().clear_cache();
|
||||
thread_manager.skills_manager().clear_cache();
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ExternalAgentConfigImportCompleted(
|
||||
ExternalAgentConfigImportCompletedNotification {},
|
||||
))
|
||||
.await;
|
||||
});
|
||||
|
||||
async fn handle_fs_read_file(&self, request_id: ConnectionRequestId, params: FsReadFileParams) {
|
||||
match self.fs_api.read_file(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_write_file(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: FsWriteFileParams,
|
||||
) {
|
||||
match self.fs_api.write_file(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_create_directory(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: FsCreateDirectoryParams,
|
||||
) {
|
||||
match self.fs_api.create_directory(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_get_metadata(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: FsGetMetadataParams,
|
||||
) {
|
||||
match self.fs_api.get_metadata(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_read_directory(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: FsReadDirectoryParams,
|
||||
) {
|
||||
match self.fs_api.read_directory(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_remove(&self, request_id: ConnectionRequestId, params: FsRemoveParams) {
|
||||
match self.fs_api.remove(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_copy(&self, request_id: ConnectionRequestId, params: FsCopyParams) {
|
||||
match self.fs_api.copy(params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_watch(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
connection_id: ConnectionId,
|
||||
params: FsWatchParams,
|
||||
) {
|
||||
match self.fs_watch_manager.watch(connection_id, params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_fs_unwatch(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
connection_id: ConnectionId,
|
||||
params: FsUnwatchParams,
|
||||
) {
|
||||
match self.fs_watch_manager.unwatch(connection_id, params).await {
|
||||
Ok(response) => self.outgoing.send_response(request_id, response).await,
|
||||
Err(error) => self.outgoing.send_error(request_id, error).await,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_login::AuthManager;
|
||||
|
||||
@@ -22,6 +22,7 @@ use tracing::Span;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::internal_error;
|
||||
use crate::server_request_error::TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -196,7 +197,7 @@ impl ThreadScopedOutgoingMessageSender {
|
||||
pub(crate) async fn send_error(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
error: JSONRPCErrorError,
|
||||
error: impl Into<JSONRPCErrorError>,
|
||||
) {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
@@ -493,11 +494,7 @@ impl OutgoingMessageSender {
|
||||
self.send_error_inner(
|
||||
request_context,
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to serialize response: {err}"),
|
||||
data: None,
|
||||
},
|
||||
internal_error(format!("failed to serialize response: {err}")),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -571,13 +568,27 @@ impl OutgoingMessageSender {
|
||||
pub(crate) async fn send_error(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
error: JSONRPCErrorError,
|
||||
error: impl Into<JSONRPCErrorError>,
|
||||
) {
|
||||
let request_context = self.take_request_context(&request_id).await;
|
||||
self.send_error_inner(request_context, request_id, error)
|
||||
self.send_error_inner(request_context, request_id, error.into())
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn send_result<T, E>(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
result: std::result::Result<T, E>,
|
||||
) where
|
||||
T: Serialize,
|
||||
E: Into<JSONRPCErrorError>,
|
||||
{
|
||||
match result {
|
||||
Ok(response) => self.send_response(request_id, response).await,
|
||||
Err(error) => self.send_error(request_id, error).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_error_inner(
|
||||
&self,
|
||||
request_context: Option<RequestContext>,
|
||||
|
||||
@@ -134,7 +134,6 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "REMOTE_COMPACT_SUMMARY".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Compaction {
|
||||
|
||||
@@ -16,9 +16,9 @@ use codex_app_server_protocol::ExperimentalFeatureStage;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Stage;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -17,6 +17,9 @@ use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[cfg(windows)]
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(25);
|
||||
#[cfg(not(windows))]
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";
|
||||
|
||||
@@ -63,13 +66,14 @@ fn commit_marketplace_marker(root: &Path, marker: &str) -> Result<String> {
|
||||
fn configured_git_marketplace_update<'a>(
|
||||
source: &'a str,
|
||||
last_revision: Option<&'a str>,
|
||||
ref_name: Option<&'a str>,
|
||||
) -> MarketplaceConfigUpdate<'a> {
|
||||
MarketplaceConfigUpdate {
|
||||
last_updated: "2026-04-13T00:00:00Z",
|
||||
last_revision,
|
||||
source_type: "git",
|
||||
source,
|
||||
ref_name: None,
|
||||
ref_name,
|
||||
sparse_paths: &[],
|
||||
}
|
||||
}
|
||||
@@ -90,12 +94,13 @@ fn record_git_marketplace(
|
||||
marketplace_name: &str,
|
||||
source: &Path,
|
||||
last_revision: &str,
|
||||
ref_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let source = source.display().to_string();
|
||||
record_user_marketplace(
|
||||
codex_home,
|
||||
marketplace_name,
|
||||
&configured_git_marketplace_update(&source, Some(last_revision)),
|
||||
&configured_git_marketplace_update(&source, Some(last_revision), ref_name),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -153,12 +158,14 @@ async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> {
|
||||
"debug",
|
||||
debug_source.path(),
|
||||
&debug_old_revision,
|
||||
Some(&debug_new_revision),
|
||||
)?;
|
||||
record_git_marketplace(
|
||||
codex_home.path(),
|
||||
"tools",
|
||||
tools_source.path(),
|
||||
&tools_old_revision,
|
||||
Some(&tools_new_revision),
|
||||
)?;
|
||||
disable_plugin_startup_tasks(codex_home.path())?;
|
||||
|
||||
@@ -205,12 +212,14 @@ async fn marketplace_upgrade_named_marketplace_only() -> Result<()> {
|
||||
"debug",
|
||||
debug_source.path(),
|
||||
&debug_old_revision,
|
||||
/*ref_name*/ None,
|
||||
)?;
|
||||
record_git_marketplace(
|
||||
codex_home.path(),
|
||||
"tools",
|
||||
tools_source.path(),
|
||||
&tools_old_revision,
|
||||
/*ref_name*/ None,
|
||||
)?;
|
||||
disable_plugin_startup_tasks(codex_home.path())?;
|
||||
|
||||
@@ -246,7 +255,13 @@ async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Re
|
||||
let source = TempDir::new()?;
|
||||
let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?;
|
||||
commit_marketplace_marker(source.path(), "debug new")?;
|
||||
record_git_marketplace(codex_home.path(), "debug", source.path(), &old_revision)?;
|
||||
record_git_marketplace(
|
||||
codex_home.path(),
|
||||
"debug",
|
||||
source.path(),
|
||||
&old_revision,
|
||||
/*ref_name*/ None,
|
||||
)?;
|
||||
disable_plugin_startup_tasks(codex_home.path())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
|
||||
@@ -20,10 +20,10 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
@@ -33,10 +33,10 @@ use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::LoaderOverrides;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_feedback::CodexFeedback;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
@@ -59,7 +59,6 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: injected_text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -195,7 +194,6 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "Injected after first turn".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
let injected_value = serde_json::to_value(&injected_item)?;
|
||||
|
||||
@@ -1660,7 +1660,6 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "history override".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
@@ -2616,7 +2615,6 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: history_text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}];
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_core::config_loader::project_trust_key;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
|
||||
|
||||
@@ -749,13 +749,17 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let unsupported_write_root = TempDir::new()?;
|
||||
let disallowed_write_root = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
"http://localhost/unused",
|
||||
"never",
|
||||
&BTreeMap::from([(Feature::Personality, true)]),
|
||||
)?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("managed_config.toml"),
|
||||
"sandbox_mode = \"read-only\"\n",
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -772,7 +776,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
let unsupported_write_root = AbsolutePathBuf::from_absolute_path(unsupported_write_root.path())
|
||||
let disallowed_write_root = AbsolutePathBuf::from_absolute_path(disallowed_write_root.path())
|
||||
.expect("tempdir path should be absolute");
|
||||
|
||||
let turn_req = mcp
|
||||
@@ -787,7 +791,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
|
||||
file_system: PermissionProfileFileSystemPermissions::Restricted {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: unsupported_write_root,
|
||||
path: disallowed_write_root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}],
|
||||
@@ -806,9 +810,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
|
||||
assert_eq!(err.error.code, INVALID_REQUEST_ERROR_CODE);
|
||||
assert!(err.error.message.contains("invalid turn context override"));
|
||||
assert!(
|
||||
err.error
|
||||
.message
|
||||
.contains("filesystem writes outside the workspace root")
|
||||
err.error.message.contains("allowed set [ReadOnly]"),
|
||||
"unexpected error message: {}",
|
||||
err.error.message
|
||||
);
|
||||
let turn_started = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(250),
|
||||
|
||||
@@ -171,7 +171,7 @@ async fn run_command_under_sandbox(
|
||||
let network_proxy = match config.permissions.network.as_ref() {
|
||||
Some(spec) => Some(
|
||||
spec.start_proxy(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
config.permissions.permission_profile.get(),
|
||||
/*policy_decider*/ None,
|
||||
/*blocked_request_observer*/ None,
|
||||
managed_network_requirements_enabled,
|
||||
@@ -189,22 +189,23 @@ async fn run_command_under_sandbox(
|
||||
let mut child = match sandbox_type {
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxType::Seatbelt => {
|
||||
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
|
||||
let network_sandbox_policy = config.permissions.network_sandbox_policy();
|
||||
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command,
|
||||
file_system_sandbox_policy: &config.permissions.file_system_sandbox_policy,
|
||||
network_sandbox_policy: config.permissions.network_sandbox_policy,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
|
||||
enforce_managed_network: false,
|
||||
network: network.as_ref(),
|
||||
extra_allow_unix_sockets: allow_unix_sockets,
|
||||
});
|
||||
let network_policy = config.permissions.network_sandbox_policy;
|
||||
spawn_debug_sandbox_child(
|
||||
PathBuf::from("/usr/bin/sandbox-exec"),
|
||||
args,
|
||||
/*arg0*/ None,
|
||||
cwd.to_path_buf(),
|
||||
network_policy,
|
||||
network_sandbox_policy,
|
||||
env,
|
||||
|env_map| {
|
||||
env_map.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
@@ -221,23 +222,26 @@ 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(
|
||||
command,
|
||||
cwd.as_path(),
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy,
|
||||
&config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(sandbox_policy_cwd.as_path()),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
/*allow_network_for_proxy*/ false,
|
||||
);
|
||||
let network_policy = config.permissions.network_sandbox_policy;
|
||||
spawn_debug_sandbox_child(
|
||||
codex_linux_sandbox_exe,
|
||||
args,
|
||||
Some("codex-linux-sandbox"),
|
||||
cwd.to_path_buf(),
|
||||
network_policy,
|
||||
network_sandbox_policy,
|
||||
env,
|
||||
|env_map| {
|
||||
if let Some(network) = network.as_ref() {
|
||||
@@ -288,7 +292,10 @@ async fn run_command_under_windows_session(
|
||||
use codex_windows_sandbox::spawn_windows_sandbox_session_elevated;
|
||||
use codex_windows_sandbox::spawn_windows_sandbox_session_legacy;
|
||||
|
||||
let policy_str = match serde_json::to_string(config.permissions.sandbox_policy.get()) {
|
||||
let sandbox_policy = config
|
||||
.permissions
|
||||
.legacy_sandbox_policy(sandbox_policy_cwd.as_path());
|
||||
let policy_str = match serde_json::to_string(&sandbox_policy) {
|
||||
Ok(policy_str) => policy_str,
|
||||
Err(err) => {
|
||||
eprintln!("windows sandbox failed to serialize policy: {err}");
|
||||
@@ -715,17 +722,17 @@ mod tests {
|
||||
|
||||
assert!(config_uses_permission_profiles(&config));
|
||||
assert!(
|
||||
profile_config.permissions.file_system_sandbox_policy
|
||||
!= legacy_config.permissions.file_system_sandbox_policy,
|
||||
profile_config.permissions.file_system_sandbox_policy()
|
||||
!= legacy_config.permissions.file_system_sandbox_policy(),
|
||||
"test fixture should distinguish profile syntax from legacy sandbox_mode"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
profile_config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
profile_config.permissions.file_system_sandbox_policy(),
|
||||
);
|
||||
assert_ne!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
legacy_config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
legacy_config.permissions.file_system_sandbox_policy(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -830,7 +830,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
codex_app_server::run_main_with_transport(
|
||||
arg0_paths.clone(),
|
||||
root_config_overrides,
|
||||
codex_core::config_loader::LoaderOverrides::default(),
|
||||
codex_config::LoaderOverrides::default(),
|
||||
analytics_default_enabled,
|
||||
transport,
|
||||
codex_protocol::protocol::SessionSource::VSCode,
|
||||
@@ -1551,7 +1551,7 @@ async fn run_interactive_tui(
|
||||
codex_tui::run_main(
|
||||
interactive,
|
||||
arg0_paths,
|
||||
codex_core::config_loader::LoaderOverrides::default(),
|
||||
codex_config::LoaderOverrides::default(),
|
||||
normalized_remote,
|
||||
remote_auth_token,
|
||||
)
|
||||
|
||||
@@ -15,11 +15,11 @@ use chrono::DateTime;
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::Utc;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_config::CloudRequirementsLoadError;
|
||||
use codex_config::CloudRequirementsLoadErrorCode;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config_loader::CloudRequirementsLoadError;
|
||||
use codex_core::config_loader::CloudRequirementsLoadErrorCode;
|
||||
use codex_core::config_loader::CloudRequirementsLoader;
|
||||
use codex_core::config_loader::ConfigRequirementsToml;
|
||||
use codex_core::util::backoff;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -1314,10 +1314,10 @@ enabled = false
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(ConfigRequirementsToml {
|
||||
apps: Some(codex_core::config_loader::AppsRequirementsToml {
|
||||
apps: Some(codex_config::AppsRequirementsToml {
|
||||
apps: BTreeMap::from([(
|
||||
"connector_5f3c8c41a1e54ad7a76272c89e2554fa".to_string(),
|
||||
codex_core::config_loader::AppRequirementToml {
|
||||
codex_config::AppRequirementToml {
|
||||
enabled: Some(false),
|
||||
},
|
||||
)]),
|
||||
|
||||
@@ -423,7 +423,6 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
|
||||
id: Some("msg_1".into()),
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText { text: "hi".into() }],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}],
|
||||
tools: Vec::new(),
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
//! Codex Apps support for the built-in apps MCP server.
|
||||
//!
|
||||
//! This module owns the pieces that are unique to ChatGPT-hosted app
|
||||
//! connectors: cache scoping by authenticated user, disk cache reads/writes,
|
||||
//! connector allow-list filtering, and the normalization that turns app
|
||||
//! connector/tool metadata into model-visible MCP callable names.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::MCP_TOOLS_CACHE_WRITE_DURATION_METRIC;
|
||||
use crate::tools::ToolInfo;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_utils_plugins::mcp_connector::is_connector_id_allowed;
|
||||
use codex_utils_plugins::mcp_connector::sanitize_name;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
|
||||
pub(crate) const CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION: u8 = 2;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CodexAppsToolsCacheKey {
|
||||
pub(crate) account_id: Option<String>,
|
||||
pub(crate) chatgpt_user_id: Option<String>,
|
||||
pub(crate) is_workspace_account: bool,
|
||||
}
|
||||
|
||||
pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCacheKey {
|
||||
CodexAppsToolsCacheKey {
|
||||
account_id: auth.and_then(CodexAuth::get_account_id),
|
||||
chatgpt_user_id: auth.and_then(CodexAuth::get_chatgpt_user_id),
|
||||
is_workspace_account: auth.is_some_and(CodexAuth::is_workspace_account),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_non_codex_apps_mcp_tools_only(
|
||||
mcp_tools: &HashMap<String, ToolInfo>,
|
||||
) -> HashMap<String, ToolInfo> {
|
||||
mcp_tools
|
||||
.iter()
|
||||
.filter(|(_, tool)| tool.server_name != CODEX_APPS_MCP_SERVER_NAME)
|
||||
.map(|(name, tool)| (name.clone(), tool.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CodexAppsToolsCacheContext {
|
||||
pub(crate) codex_home: PathBuf,
|
||||
pub(crate) user_key: CodexAppsToolsCacheKey,
|
||||
}
|
||||
|
||||
impl CodexAppsToolsCacheContext {
|
||||
pub(crate) fn cache_path(&self) -> PathBuf {
|
||||
let user_key_json = serde_json::to_string(&self.user_key).unwrap_or_default();
|
||||
let user_key_hash = sha1_hex(&user_key_json);
|
||||
self.codex_home
|
||||
.join(CODEX_APPS_TOOLS_CACHE_DIR)
|
||||
.join(format!("{user_key_hash}.json"))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum CachedCodexAppsToolsLoad {
|
||||
Hit(Vec<ToolInfo>),
|
||||
Missing,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_tool_title(
|
||||
server_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
value: &str,
|
||||
) -> String {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return value.to_string();
|
||||
}
|
||||
|
||||
let Some(connector_name) = connector_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
else {
|
||||
return value.to_string();
|
||||
};
|
||||
|
||||
let prefix = format!("{connector_name}_");
|
||||
if let Some(stripped) = value.strip_prefix(&prefix)
|
||||
&& !stripped.is_empty()
|
||||
{
|
||||
return stripped.to_string();
|
||||
}
|
||||
|
||||
value.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_callable_name(
|
||||
server_name: &str,
|
||||
tool_name: &str,
|
||||
connector_id: Option<&str>,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return tool_name.to_string();
|
||||
}
|
||||
|
||||
let tool_name = sanitize_name(tool_name);
|
||||
|
||||
if let Some(connector_name) = connector_name
|
||||
.map(str::trim)
|
||||
.map(sanitize_name)
|
||||
.filter(|name| !name.is_empty())
|
||||
&& let Some(stripped) = tool_name.strip_prefix(&connector_name)
|
||||
&& !stripped.is_empty()
|
||||
{
|
||||
return stripped.to_string();
|
||||
}
|
||||
|
||||
if let Some(connector_id) = connector_id
|
||||
.map(str::trim)
|
||||
.map(sanitize_name)
|
||||
.filter(|name| !name.is_empty())
|
||||
&& let Some(stripped) = tool_name.strip_prefix(&connector_id)
|
||||
&& !stripped.is_empty()
|
||||
{
|
||||
return stripped.to_string();
|
||||
}
|
||||
|
||||
tool_name
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_codex_apps_callable_namespace(
|
||||
server_name: &str,
|
||||
connector_name: Option<&str>,
|
||||
) -> String {
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& let Some(connector_name) = connector_name
|
||||
{
|
||||
format!("mcp__{}__{}", server_name, sanitize_name(connector_name))
|
||||
} else {
|
||||
format!("mcp__{server_name}__")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_cached_codex_apps_tools_if_needed(
|
||||
server_name: &str,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
tools: &[ToolInfo],
|
||||
) {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(cache_context) = cache_context {
|
||||
let cache_write_start = Instant::now();
|
||||
write_cached_codex_apps_tools(cache_context, tools);
|
||||
emit_duration(
|
||||
MCP_TOOLS_CACHE_WRITE_DURATION_METRIC,
|
||||
cache_write_start.elapsed(),
|
||||
&[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_startup_cached_codex_apps_tools_snapshot(
|
||||
server_name: &str,
|
||||
cache_context: Option<&CodexAppsToolsCacheContext>,
|
||||
) -> Option<Vec<ToolInfo>> {
|
||||
if server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cache_context = cache_context?;
|
||||
|
||||
match load_cached_codex_apps_tools(cache_context) {
|
||||
CachedCodexAppsToolsLoad::Hit(tools) => Some(tools),
|
||||
CachedCodexAppsToolsLoad::Missing | CachedCodexAppsToolsLoad::Invalid => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_cached_codex_apps_tools(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
) -> Option<Vec<ToolInfo>> {
|
||||
match load_cached_codex_apps_tools(cache_context) {
|
||||
CachedCodexAppsToolsLoad::Hit(tools) => Some(tools),
|
||||
CachedCodexAppsToolsLoad::Missing | CachedCodexAppsToolsLoad::Invalid => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_cached_codex_apps_tools(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
) -> CachedCodexAppsToolsLoad {
|
||||
let cache_path = cache_context.cache_path();
|
||||
let bytes = match std::fs::read(cache_path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
return CachedCodexAppsToolsLoad::Missing;
|
||||
}
|
||||
Err(_) => return CachedCodexAppsToolsLoad::Invalid,
|
||||
};
|
||||
let cache: CodexAppsToolsDiskCache = match serde_json::from_slice(&bytes) {
|
||||
Ok(cache) => cache,
|
||||
Err(_) => return CachedCodexAppsToolsLoad::Invalid,
|
||||
};
|
||||
if cache.schema_version != CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION {
|
||||
return CachedCodexAppsToolsLoad::Invalid;
|
||||
}
|
||||
CachedCodexAppsToolsLoad::Hit(filter_disallowed_codex_apps_tools(cache.tools))
|
||||
}
|
||||
|
||||
pub(crate) fn write_cached_codex_apps_tools(
|
||||
cache_context: &CodexAppsToolsCacheContext,
|
||||
tools: &[ToolInfo],
|
||||
) {
|
||||
let cache_path = cache_context.cache_path();
|
||||
if let Some(parent) = cache_path.parent()
|
||||
&& std::fs::create_dir_all(parent).is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let tools = filter_disallowed_codex_apps_tools(tools.to_vec());
|
||||
let Ok(bytes) = serde_json::to_vec_pretty(&CodexAppsToolsDiskCache {
|
||||
schema_version: CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION,
|
||||
tools,
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let _ = std::fs::write(cache_path, bytes);
|
||||
}
|
||||
|
||||
pub(crate) fn filter_disallowed_codex_apps_tools(tools: Vec<ToolInfo>) -> Vec<ToolInfo> {
|
||||
tools
|
||||
.into_iter()
|
||||
.filter(|tool| {
|
||||
tool.connector_id
|
||||
.as_deref()
|
||||
.is_none_or(is_connector_id_allowed)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CodexAppsToolsDiskCache {
|
||||
schema_version: u8,
|
||||
tools: Vec<ToolInfo>,
|
||||
}
|
||||
|
||||
const CODEX_APPS_TOOLS_CACHE_DIR: &str = "cache/codex_apps_tools";
|
||||
|
||||
fn sha1_hex(s: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(s.as_bytes());
|
||||
let sha1 = hasher.finalize();
|
||||
format!("{sha1:x}")
|
||||
}
|
||||
@@ -0,0 +1,700 @@
|
||||
//! Aggregates MCP server connections for Codex.
|
||||
//!
|
||||
//! [`McpConnectionManager`] owns the set of running async RMCP clients keyed by
|
||||
//! MCP server name. It coordinates startup status events, keeps server origin
|
||||
//! metadata, aggregates tools/resources/templates across servers, routes tool
|
||||
//! calls to the right client, and exposes the public manager API used by
|
||||
//! `codex-core`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::McpAuthStatusEntry;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::CodexAppsToolsCacheKey;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::rmcp_client::AsyncManagedClient;
|
||||
use crate::rmcp_client::DEFAULT_STARTUP_TIMEOUT;
|
||||
use crate::rmcp_client::MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC;
|
||||
use crate::rmcp_client::MCP_TOOLS_LIST_DURATION_METRIC;
|
||||
use crate::rmcp_client::ManagedClient;
|
||||
use crate::rmcp_client::StartupOutcomeError;
|
||||
use crate::rmcp_client::list_tools_for_client_uncached;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::ToolInfo;
|
||||
use crate::tools::filter_tools;
|
||||
use crate::tools::qualify_tools;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::McpStartupCompleteEvent;
|
||||
use codex_protocol::protocol::McpStartupFailure;
|
||||
use codex_protocol::protocol::McpStartupStatus;
|
||||
use codex_protocol::protocol::McpStartupUpdateEvent;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::PaginatedRequestParams;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::RequestId;
|
||||
use rmcp::model::Resource;
|
||||
use rmcp::model::ResourceTemplate;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::instrument;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
/// A thin wrapper around a set of running [`RmcpClient`] instances.
|
||||
pub struct McpConnectionManager {
|
||||
clients: HashMap<String, AsyncManagedClient>,
|
||||
server_origins: HashMap<String, String>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
}
|
||||
|
||||
impl McpConnectionManager {
|
||||
pub fn new_uninitialized(
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
permission_profile: &Constrained<PermissionProfile>,
|
||||
) -> Self {
|
||||
Self {
|
||||
clients: HashMap::new(),
|
||||
server_origins: HashMap::new(),
|
||||
elicitation_requests: ElicitationRequestManager::new(
|
||||
approval_policy.value(),
|
||||
permission_profile.get().clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_servers(&self) -> bool {
|
||||
!self.clients.is_empty()
|
||||
}
|
||||
|
||||
pub fn server_origin(&self, server_name: &str) -> Option<&str> {
|
||||
self.server_origins.get(server_name).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
|
||||
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
|
||||
*policy = approval_policy.value();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_permission_profile(&self, permission_profile: PermissionProfile) {
|
||||
if let Ok(mut profile) = self.elicitation_requests.permission_profile.lock() {
|
||||
*profile = permission_profile;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::new_ret_no_self, clippy::too_many_arguments)]
|
||||
pub async fn new(
|
||||
mcp_servers: &HashMap<String, McpServerConfig>,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
auth_entries: HashMap<String, McpAuthStatusEntry>,
|
||||
approval_policy: &Constrained<AskForApproval>,
|
||||
submit_id: String,
|
||||
tx_event: Sender<Event>,
|
||||
initial_permission_profile: PermissionProfile,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
codex_home: PathBuf,
|
||||
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
|
||||
tool_plugin_provenance: ToolPluginProvenance,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> (Self, CancellationToken) {
|
||||
let cancel_token = CancellationToken::new();
|
||||
let mut clients = HashMap::new();
|
||||
let mut server_origins = HashMap::new();
|
||||
let mut join_set = JoinSet::new();
|
||||
let elicitation_requests =
|
||||
ElicitationRequestManager::new(approval_policy.value(), initial_permission_profile);
|
||||
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
|
||||
let startup_submit_id = submit_id.clone();
|
||||
let codex_apps_auth_provider = auth
|
||||
.filter(|auth| auth.uses_codex_backend())
|
||||
.map(codex_model_provider::auth_provider_from_auth);
|
||||
let mcp_servers = mcp_servers.clone();
|
||||
for (server_name, cfg) in mcp_servers.into_iter().filter(|(_, cfg)| cfg.enabled) {
|
||||
if let Some(origin) = transport_origin(&cfg.transport) {
|
||||
server_origins.insert(server_name.clone(), origin);
|
||||
}
|
||||
let cancel_token = cancel_token.child_token();
|
||||
let _ = emit_update(
|
||||
startup_submit_id.as_str(),
|
||||
&tx_event,
|
||||
McpStartupUpdateEvent {
|
||||
server: server_name.clone(),
|
||||
status: McpStartupStatus::Starting,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let codex_apps_tools_cache_context = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
Some(CodexAppsToolsCacheContext {
|
||||
codex_home: codex_home.clone(),
|
||||
user_key: codex_apps_tools_cache_key.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let uses_env_bearer_token = match &cfg.transport {
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
bearer_token_env_var,
|
||||
..
|
||||
} => bearer_token_env_var.is_some(),
|
||||
McpServerTransportConfig::Stdio { .. } => false,
|
||||
};
|
||||
let runtime_auth_provider =
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME && !uses_env_bearer_token {
|
||||
codex_apps_auth_provider.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let async_managed_client = AsyncManagedClient::new(
|
||||
server_name.clone(),
|
||||
cfg,
|
||||
store_mode,
|
||||
cancel_token.clone(),
|
||||
tx_event.clone(),
|
||||
elicitation_requests.clone(),
|
||||
codex_apps_tools_cache_context,
|
||||
Arc::clone(&tool_plugin_provenance),
|
||||
runtime_environment.clone(),
|
||||
runtime_auth_provider,
|
||||
);
|
||||
clients.insert(server_name.clone(), async_managed_client.clone());
|
||||
let tx_event = tx_event.clone();
|
||||
let submit_id = startup_submit_id.clone();
|
||||
let auth_entry = auth_entries.get(&server_name).cloned();
|
||||
join_set.spawn(async move {
|
||||
let mut outcome = async_managed_client.client().await;
|
||||
if cancel_token.is_cancelled() {
|
||||
outcome = Err(StartupOutcomeError::Cancelled);
|
||||
}
|
||||
let status = match &outcome {
|
||||
Ok(_) => McpStartupStatus::Ready,
|
||||
Err(StartupOutcomeError::Cancelled) => McpStartupStatus::Cancelled,
|
||||
Err(error) => {
|
||||
let error_str = mcp_init_error_display(
|
||||
server_name.as_str(),
|
||||
auth_entry.as_ref(),
|
||||
error,
|
||||
);
|
||||
McpStartupStatus::Failed { error: error_str }
|
||||
}
|
||||
};
|
||||
|
||||
let _ = emit_update(
|
||||
submit_id.as_str(),
|
||||
&tx_event,
|
||||
McpStartupUpdateEvent {
|
||||
server: server_name.clone(),
|
||||
status,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
(server_name, outcome)
|
||||
});
|
||||
}
|
||||
let manager = Self {
|
||||
clients,
|
||||
server_origins,
|
||||
elicitation_requests: elicitation_requests.clone(),
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
let outcomes = join_set.join_all().await;
|
||||
let mut summary = McpStartupCompleteEvent::default();
|
||||
for (server_name, outcome) in outcomes {
|
||||
match outcome {
|
||||
Ok(_) => summary.ready.push(server_name),
|
||||
Err(StartupOutcomeError::Cancelled) => summary.cancelled.push(server_name),
|
||||
Err(StartupOutcomeError::Failed { error }) => {
|
||||
summary.failed.push(McpStartupFailure {
|
||||
server: server_name,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = tx_event
|
||||
.send(Event {
|
||||
id: startup_submit_id,
|
||||
msg: EventMsg::McpStartupComplete(summary),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
(manager, cancel_token)
|
||||
}
|
||||
|
||||
pub async fn resolve_elicitation(
|
||||
&self,
|
||||
server_name: String,
|
||||
id: RequestId,
|
||||
response: ElicitationResponse,
|
||||
) -> Result<()> {
|
||||
self.elicitation_requests
|
||||
.resolve(server_name, id, response)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn wait_for_server_ready(&self, server_name: &str, timeout: Duration) -> bool {
|
||||
let Some(async_managed_client) = self.clients.get(server_name) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match tokio::time::timeout(timeout, async_managed_client.client()).await {
|
||||
Ok(Ok(_)) => true,
|
||||
Ok(Err(_)) | Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn required_startup_failures(
|
||||
&self,
|
||||
required_servers: &[String],
|
||||
) -> Vec<McpStartupFailure> {
|
||||
let mut failures = Vec::new();
|
||||
for server_name in required_servers {
|
||||
let Some(async_managed_client) = self.clients.get(server_name).cloned() else {
|
||||
failures.push(McpStartupFailure {
|
||||
server: server_name.clone(),
|
||||
error: format!("required MCP server `{server_name}` was not initialized"),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
match async_managed_client.client().await {
|
||||
Ok(_) => {}
|
||||
Err(error) => failures.push(McpStartupFailure {
|
||||
server: server_name.clone(),
|
||||
error: startup_outcome_error_message(error),
|
||||
}),
|
||||
}
|
||||
}
|
||||
failures
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all tools. Each key is the
|
||||
/// fully-qualified name for the tool.
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn list_all_tools(&self) -> HashMap<String, ToolInfo> {
|
||||
let mut tools = Vec::new();
|
||||
for managed_client in self.clients.values() {
|
||||
let Some(server_tools) = managed_client.listed_tools().await else {
|
||||
continue;
|
||||
};
|
||||
tools.extend(server_tools);
|
||||
}
|
||||
qualify_tools(tools)
|
||||
}
|
||||
|
||||
/// Force-refresh codex apps tools by bypassing the in-process cache.
|
||||
///
|
||||
/// On success, the refreshed tools replace the cache contents and the
|
||||
/// latest filtered tool map is returned directly to the caller. On
|
||||
/// failure, the existing cache remains unchanged.
|
||||
pub async fn hard_refresh_codex_apps_tools_cache(&self) -> Result<HashMap<String, ToolInfo>> {
|
||||
let managed_client = self
|
||||
.clients
|
||||
.get(CODEX_APPS_MCP_SERVER_NAME)
|
||||
.ok_or_else(|| anyhow!("unknown MCP server '{CODEX_APPS_MCP_SERVER_NAME}'"))?
|
||||
.client()
|
||||
.await
|
||||
.context("failed to get client")?;
|
||||
|
||||
let list_start = Instant::now();
|
||||
let fetch_start = Instant::now();
|
||||
let tools = list_tools_for_client_uncached(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
&managed_client.client,
|
||||
managed_client.tool_timeout,
|
||||
managed_client.server_instructions.as_deref(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("failed to refresh tools for MCP server '{CODEX_APPS_MCP_SERVER_NAME}'")
|
||||
})?;
|
||||
emit_duration(
|
||||
MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC,
|
||||
fetch_start.elapsed(),
|
||||
&[],
|
||||
);
|
||||
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
managed_client.codex_apps_tools_cache_context.as_ref(),
|
||||
&tools,
|
||||
);
|
||||
emit_duration(
|
||||
MCP_TOOLS_LIST_DURATION_METRIC,
|
||||
list_start.elapsed(),
|
||||
&[("cache", "miss")],
|
||||
);
|
||||
let tools = filter_tools(tools, &managed_client.tool_filter)
|
||||
.into_iter()
|
||||
.map(|mut tool| {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
tool
|
||||
});
|
||||
Ok(qualify_tools(tools))
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all resources. Each key is the
|
||||
/// server name and the value is a vector of resources.
|
||||
pub async fn list_all_resources(&self) -> HashMap<String, Vec<Resource>> {
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
let clients_snapshot = &self.clients;
|
||||
|
||||
for (server_name, async_managed_client) in clients_snapshot {
|
||||
let server_name = server_name.clone();
|
||||
let Ok(managed_client) = async_managed_client.client().await else {
|
||||
continue;
|
||||
};
|
||||
let timeout = managed_client.tool_timeout;
|
||||
let client = managed_client.client.clone();
|
||||
|
||||
join_set.spawn(async move {
|
||||
let mut collected: Vec<Resource> = Vec::new();
|
||||
let mut cursor: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let params = cursor.as_ref().map(|next| PaginatedRequestParams {
|
||||
meta: None,
|
||||
cursor: Some(next.clone()),
|
||||
});
|
||||
let response = match client.list_resources(params, timeout).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => return (server_name, Err(err)),
|
||||
};
|
||||
|
||||
collected.extend(response.resources);
|
||||
|
||||
match response.next_cursor {
|
||||
Some(next) => {
|
||||
if cursor.as_ref() == Some(&next) {
|
||||
return (
|
||||
server_name,
|
||||
Err(anyhow!("resources/list returned duplicate cursor")),
|
||||
);
|
||||
}
|
||||
cursor = Some(next);
|
||||
}
|
||||
None => return (server_name, Ok(collected)),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut aggregated: HashMap<String, Vec<Resource>> = HashMap::new();
|
||||
|
||||
while let Some(join_res) = join_set.join_next().await {
|
||||
match join_res {
|
||||
Ok((server_name, Ok(resources))) => {
|
||||
aggregated.insert(server_name, resources);
|
||||
}
|
||||
Ok((server_name, Err(err))) => {
|
||||
warn!("Failed to list resources for MCP server '{server_name}': {err:#}");
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Task panic when listing resources for MCP server: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aggregated
|
||||
}
|
||||
|
||||
/// Returns a single map that contains all resource templates. Each key is the
|
||||
/// server name and the value is a vector of resource templates.
|
||||
pub async fn list_all_resource_templates(&self) -> HashMap<String, Vec<ResourceTemplate>> {
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
let clients_snapshot = &self.clients;
|
||||
|
||||
for (server_name, async_managed_client) in clients_snapshot {
|
||||
let server_name_cloned = server_name.clone();
|
||||
let Ok(managed_client) = async_managed_client.client().await else {
|
||||
continue;
|
||||
};
|
||||
let client = managed_client.client.clone();
|
||||
let timeout = managed_client.tool_timeout;
|
||||
|
||||
join_set.spawn(async move {
|
||||
let mut collected: Vec<ResourceTemplate> = Vec::new();
|
||||
let mut cursor: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let params = cursor.as_ref().map(|next| PaginatedRequestParams {
|
||||
meta: None,
|
||||
cursor: Some(next.clone()),
|
||||
});
|
||||
let response = match client.list_resource_templates(params, timeout).await {
|
||||
Ok(result) => result,
|
||||
Err(err) => return (server_name_cloned, Err(err)),
|
||||
};
|
||||
|
||||
collected.extend(response.resource_templates);
|
||||
|
||||
match response.next_cursor {
|
||||
Some(next) => {
|
||||
if cursor.as_ref() == Some(&next) {
|
||||
return (
|
||||
server_name_cloned,
|
||||
Err(anyhow!(
|
||||
"resources/templates/list returned duplicate cursor"
|
||||
)),
|
||||
);
|
||||
}
|
||||
cursor = Some(next);
|
||||
}
|
||||
None => return (server_name_cloned, Ok(collected)),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut aggregated: HashMap<String, Vec<ResourceTemplate>> = HashMap::new();
|
||||
|
||||
while let Some(join_res) = join_set.join_next().await {
|
||||
match join_res {
|
||||
Ok((server_name, Ok(templates))) => {
|
||||
aggregated.insert(server_name, templates);
|
||||
}
|
||||
Ok((server_name, Err(err))) => {
|
||||
warn!(
|
||||
"Failed to list resource templates for MCP server '{server_name}': {err:#}"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Task panic when listing resource templates for MCP server: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aggregated
|
||||
}
|
||||
|
||||
/// Invoke the tool indicated by the (server, tool) pair.
|
||||
pub async fn call_tool(
|
||||
&self,
|
||||
server: &str,
|
||||
tool: &str,
|
||||
arguments: Option<serde_json::Value>,
|
||||
meta: Option<serde_json::Value>,
|
||||
) -> Result<CallToolResult> {
|
||||
let client = self.client_by_name(server).await?;
|
||||
if !client.tool_filter.allows(tool) {
|
||||
return Err(anyhow!(
|
||||
"tool '{tool}' is disabled for MCP server '{server}'"
|
||||
));
|
||||
}
|
||||
|
||||
let result: rmcp::model::CallToolResult = client
|
||||
.client
|
||||
.call_tool(tool.to_string(), arguments, meta, client.tool_timeout)
|
||||
.await
|
||||
.with_context(|| format!("tool call failed for `{server}/{tool}`"))?;
|
||||
|
||||
let content = result
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|content| {
|
||||
serde_json::to_value(content)
|
||||
.unwrap_or_else(|_| serde_json::Value::String("<content>".to_string()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(CallToolResult {
|
||||
content,
|
||||
structured_content: result.structured_content,
|
||||
is_error: result.is_error,
|
||||
meta: result.meta.and_then(|meta| serde_json::to_value(meta).ok()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn server_supports_sandbox_state_meta_capability(
|
||||
&self,
|
||||
server: &str,
|
||||
) -> Result<bool> {
|
||||
Ok(self
|
||||
.client_by_name(server)
|
||||
.await?
|
||||
.server_supports_sandbox_state_meta_capability)
|
||||
}
|
||||
|
||||
/// List resources from the specified server.
|
||||
pub async fn list_resources(
|
||||
&self,
|
||||
server: &str,
|
||||
params: Option<PaginatedRequestParams>,
|
||||
) -> Result<ListResourcesResult> {
|
||||
let managed = self.client_by_name(server).await?;
|
||||
let timeout = managed.tool_timeout;
|
||||
|
||||
managed
|
||||
.client
|
||||
.list_resources(params, timeout)
|
||||
.await
|
||||
.with_context(|| format!("resources/list failed for `{server}`"))
|
||||
}
|
||||
|
||||
/// List resource templates from the specified server.
|
||||
pub async fn list_resource_templates(
|
||||
&self,
|
||||
server: &str,
|
||||
params: Option<PaginatedRequestParams>,
|
||||
) -> Result<ListResourceTemplatesResult> {
|
||||
let managed = self.client_by_name(server).await?;
|
||||
let client = managed.client.clone();
|
||||
let timeout = managed.tool_timeout;
|
||||
|
||||
client
|
||||
.list_resource_templates(params, timeout)
|
||||
.await
|
||||
.with_context(|| format!("resources/templates/list failed for `{server}`"))
|
||||
}
|
||||
|
||||
/// Read a resource from the specified server.
|
||||
pub async fn read_resource(
|
||||
&self,
|
||||
server: &str,
|
||||
params: ReadResourceRequestParams,
|
||||
) -> Result<ReadResourceResult> {
|
||||
let managed = self.client_by_name(server).await?;
|
||||
let client = managed.client.clone();
|
||||
let timeout = managed.tool_timeout;
|
||||
let uri = params.uri.clone();
|
||||
|
||||
client
|
||||
.read_resource(params, timeout)
|
||||
.await
|
||||
.with_context(|| format!("resources/read failed for `{server}` ({uri})"))
|
||||
}
|
||||
|
||||
pub async fn resolve_tool_info(&self, tool_name: &ToolName) -> Option<ToolInfo> {
|
||||
let all_tools = self.list_all_tools().await;
|
||||
all_tools
|
||||
.into_values()
|
||||
.find(|tool| tool.canonical_tool_name() == *tool_name)
|
||||
}
|
||||
|
||||
async fn client_by_name(&self, name: &str) -> Result<ManagedClient> {
|
||||
self.clients
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!("unknown MCP server '{name}'"))?
|
||||
.client()
|
||||
.await
|
||||
.context("failed to get client")
|
||||
}
|
||||
}
|
||||
|
||||
async fn emit_update(
|
||||
submit_id: &str,
|
||||
tx_event: &Sender<Event>,
|
||||
update: McpStartupUpdateEvent,
|
||||
) -> Result<(), async_channel::SendError<Event>> {
|
||||
tx_event
|
||||
.send(Event {
|
||||
id: submit_id.to_string(),
|
||||
msg: EventMsg::McpStartupUpdate(update),
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
fn transport_origin(transport: &McpServerTransportConfig) -> Option<String> {
|
||||
match transport {
|
||||
McpServerTransportConfig::StreamableHttp { url, .. } => {
|
||||
let parsed = Url::parse(url).ok()?;
|
||||
Some(parsed.origin().ascii_serialization())
|
||||
}
|
||||
McpServerTransportConfig::Stdio { .. } => Some("stdio".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_init_error_display(
|
||||
server_name: &str,
|
||||
entry: Option<&McpAuthStatusEntry>,
|
||||
err: &StartupOutcomeError,
|
||||
) -> String {
|
||||
if let Some(McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
bearer_token_env_var,
|
||||
http_headers,
|
||||
..
|
||||
}) = &entry.map(|entry| &entry.config.transport)
|
||||
&& url == "https://api.githubcopilot.com/mcp/"
|
||||
&& bearer_token_env_var.is_none()
|
||||
&& http_headers.as_ref().map(HashMap::is_empty).unwrap_or(true)
|
||||
{
|
||||
format!(
|
||||
"GitHub MCP does not support OAuth. Log in by adding a personal access token (https://github.com/settings/personal-access-tokens) to your environment and config.toml:\n[mcp_servers.{server_name}]\nbearer_token_env_var = CODEX_GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
)
|
||||
} else if is_mcp_client_auth_required_error(err) {
|
||||
format!(
|
||||
"The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`."
|
||||
)
|
||||
} else if is_mcp_client_startup_timeout_error(err) {
|
||||
let startup_timeout_secs = match entry {
|
||||
Some(entry) => match entry.config.startup_timeout_sec {
|
||||
Some(timeout) => timeout,
|
||||
None => DEFAULT_STARTUP_TIMEOUT,
|
||||
},
|
||||
None => DEFAULT_STARTUP_TIMEOUT,
|
||||
}
|
||||
.as_secs();
|
||||
format!(
|
||||
"MCP client for `{server_name}` timed out after {startup_timeout_secs} seconds. Add or adjust `startup_timeout_sec` in your config.toml:\n[mcp_servers.{server_name}]\nstartup_timeout_sec = XX"
|
||||
)
|
||||
} else {
|
||||
format!("MCP client for `{server_name}` failed to start: {err:#}")
|
||||
}
|
||||
}
|
||||
|
||||
fn startup_outcome_error_message(error: StartupOutcomeError) -> String {
|
||||
match error {
|
||||
StartupOutcomeError::Cancelled => "MCP startup cancelled".to_string(),
|
||||
StartupOutcomeError::Failed { error } => error,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mcp_client_auth_required_error(error: &StartupOutcomeError) -> bool {
|
||||
match error {
|
||||
StartupOutcomeError::Failed { error } => error.contains("Auth required"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mcp_client_startup_timeout_error(error: &StartupOutcomeError) -> bool {
|
||||
match error {
|
||||
StartupOutcomeError::Failed { error } => {
|
||||
error.contains("request timed out")
|
||||
|| error.contains("timed out handshaking with MCP server")
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "connection_manager_tests.rs"]
|
||||
mod tests;
|
||||
+44
-14
@@ -1,11 +1,36 @@
|
||||
use super::*;
|
||||
use crate::codex_apps::CODEX_APPS_TOOLS_CACHE_SCHEMA_VERSION;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::read_cached_codex_apps_tools;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools;
|
||||
use crate::declared_openai_file_input_param_names;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::elicitation::elicitation_is_rejected_by_policy;
|
||||
use crate::rmcp_client::AsyncManagedClient;
|
||||
use crate::rmcp_client::ManagedClient;
|
||||
use crate::rmcp_client::StartupOutcomeError;
|
||||
use crate::rmcp_client::elicitation_capability_for_server;
|
||||
use crate::tools::ToolFilter;
|
||||
use crate::tools::ToolInfo;
|
||||
use crate::tools::filter_tools;
|
||||
use crate::tools::qualify_tools;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use codex_config::Constrained;
|
||||
use codex_protocol::ToolName;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use futures::FutureExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::FormElicitationCapability;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Meta;
|
||||
use rmcp::model::NumberOrString;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
@@ -179,9 +204,9 @@ fn elicitation_granular_policy_respects_never_and_config() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_access_auto_accepts_elicitation_with_empty_form_schema() {
|
||||
async fn disabled_permissions_auto_accept_elicitation_with_empty_form_schema() {
|
||||
let manager =
|
||||
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
|
||||
ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
|
||||
let (tx_event, _rx_event) = async_channel::bounded(1);
|
||||
let sender = manager.make_sender("server".to_string(), tx_event);
|
||||
|
||||
@@ -209,9 +234,9 @@ async fn full_access_auto_accepts_elicitation_with_empty_form_schema() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_access_does_not_auto_accept_elicitation_with_requested_fields() {
|
||||
async fn disabled_permissions_do_not_auto_accept_elicitation_with_requested_fields() {
|
||||
let manager =
|
||||
ElicitationRequestManager::new(AskForApproval::Never, SandboxPolicy::DangerFullAccess);
|
||||
ElicitationRequestManager::new(AskForApproval::Never, PermissionProfile::Disabled);
|
||||
let (tx_event, _rx_event) = async_channel::bounded(1);
|
||||
let sender = manager.make_sender("server".to_string(), tx_event);
|
||||
|
||||
@@ -627,8 +652,9 @@ async fn list_all_tools_uses_startup_snapshot_while_client_is_pending() {
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager =
|
||||
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
@@ -654,8 +680,9 @@ async fn resolve_tool_info_accepts_canonical_namespaced_tool_names() {
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager =
|
||||
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
|
||||
manager.clients.insert(
|
||||
"rmcp".to_string(),
|
||||
AsyncManagedClient {
|
||||
@@ -689,8 +716,9 @@ async fn list_all_tools_blocks_while_client_is_pending_without_startup_snapshot(
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager =
|
||||
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
@@ -712,8 +740,9 @@ async fn list_all_tools_does_not_block_when_startup_snapshot_cache_hit_is_empty(
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager =
|
||||
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
@@ -744,8 +773,9 @@ async fn list_all_tools_uses_startup_snapshot_when_client_startup_fails() {
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(&approval_policy, &sandbox_policy);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager =
|
||||
McpConnectionManager::new_uninitialized(&approval_policy, &permission_profile);
|
||||
let startup_complete = Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
@@ -0,0 +1,190 @@
|
||||
//! MCP elicitation request tracking and policy handling.
|
||||
//!
|
||||
//! RMCP clients call into this module when a server asks Codex to elicit data
|
||||
//! from the user. It decides whether the request can be automatically accepted,
|
||||
//! must be declined by policy, or should be surfaced as a Codex protocol event
|
||||
//! and later resolved through the stored responder.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
use crate::mcp::mcp_permission_prompt_is_auto_approved;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_protocol::approvals::ElicitationRequest;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::mcp::RequestId as ProtocolRequestId;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rmcp_client::SendElicitation;
|
||||
use futures::future::FutureExt;
|
||||
use rmcp::model::CreateElicitationRequestParams;
|
||||
use rmcp::model::ElicitationAction;
|
||||
use rmcp::model::RequestId;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ElicitationRequestManager {
|
||||
requests: Arc<Mutex<ResponderMap>>,
|
||||
pub(crate) approval_policy: Arc<StdMutex<AskForApproval>>,
|
||||
pub(crate) permission_profile: Arc<StdMutex<PermissionProfile>>,
|
||||
}
|
||||
|
||||
impl ElicitationRequestManager {
|
||||
pub(crate) fn new(
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
) -> Self {
|
||||
Self {
|
||||
requests: Arc::new(Mutex::new(HashMap::new())),
|
||||
approval_policy: Arc::new(StdMutex::new(approval_policy)),
|
||||
permission_profile: Arc::new(StdMutex::new(permission_profile)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve(
|
||||
&self,
|
||||
server_name: String,
|
||||
id: RequestId,
|
||||
response: ElicitationResponse,
|
||||
) -> Result<()> {
|
||||
self.requests
|
||||
.lock()
|
||||
.await
|
||||
.remove(&(server_name, id))
|
||||
.ok_or_else(|| anyhow!("elicitation request not found"))?
|
||||
.send(response)
|
||||
.map_err(|e| anyhow!("failed to send elicitation response: {e:?}"))
|
||||
}
|
||||
|
||||
pub(crate) fn make_sender(
|
||||
&self,
|
||||
server_name: String,
|
||||
tx_event: Sender<Event>,
|
||||
) -> SendElicitation {
|
||||
let elicitation_requests = self.requests.clone();
|
||||
let approval_policy = self.approval_policy.clone();
|
||||
let permission_profile = self.permission_profile.clone();
|
||||
Box::new(move |id, elicitation| {
|
||||
let elicitation_requests = elicitation_requests.clone();
|
||||
let tx_event = tx_event.clone();
|
||||
let server_name = server_name.clone();
|
||||
let approval_policy = approval_policy.clone();
|
||||
let permission_profile = permission_profile.clone();
|
||||
async move {
|
||||
let approval_policy = approval_policy
|
||||
.lock()
|
||||
.map(|policy| *policy)
|
||||
.unwrap_or(AskForApproval::Never);
|
||||
let permission_profile = permission_profile
|
||||
.lock()
|
||||
.map(|profile| profile.clone())
|
||||
.unwrap_or_default();
|
||||
if mcp_permission_prompt_is_auto_approved(approval_policy, &permission_profile)
|
||||
&& can_auto_accept_elicitation(&elicitation)
|
||||
{
|
||||
return Ok(ElicitationResponse {
|
||||
action: ElicitationAction::Accept,
|
||||
content: Some(serde_json::json!({})),
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
if elicitation_is_rejected_by_policy(approval_policy) {
|
||||
return Ok(ElicitationResponse {
|
||||
action: ElicitationAction::Decline,
|
||||
content: None,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
let request = match elicitation {
|
||||
CreateElicitationRequestParams::FormElicitationParams {
|
||||
meta,
|
||||
message,
|
||||
requested_schema,
|
||||
} => ElicitationRequest::Form {
|
||||
meta: meta
|
||||
.map(serde_json::to_value)
|
||||
.transpose()
|
||||
.context("failed to serialize MCP elicitation metadata")?,
|
||||
message,
|
||||
requested_schema: serde_json::to_value(requested_schema)
|
||||
.context("failed to serialize MCP elicitation schema")?,
|
||||
},
|
||||
CreateElicitationRequestParams::UrlElicitationParams {
|
||||
meta,
|
||||
message,
|
||||
url,
|
||||
elicitation_id,
|
||||
} => ElicitationRequest::Url {
|
||||
meta: meta
|
||||
.map(serde_json::to_value)
|
||||
.transpose()
|
||||
.context("failed to serialize MCP elicitation metadata")?,
|
||||
message,
|
||||
url,
|
||||
elicitation_id,
|
||||
},
|
||||
};
|
||||
let (tx, rx) = oneshot::channel();
|
||||
{
|
||||
let mut lock = elicitation_requests.lock().await;
|
||||
lock.insert((server_name.clone(), id.clone()), tx);
|
||||
}
|
||||
let _ = tx_event
|
||||
.send(Event {
|
||||
id: "mcp_elicitation_request".to_string(),
|
||||
msg: EventMsg::ElicitationRequest(ElicitationRequestEvent {
|
||||
turn_id: None,
|
||||
server_name,
|
||||
id: match id.clone() {
|
||||
rmcp::model::NumberOrString::String(value) => {
|
||||
ProtocolRequestId::String(value.to_string())
|
||||
}
|
||||
rmcp::model::NumberOrString::Number(value) => {
|
||||
ProtocolRequestId::Integer(value)
|
||||
}
|
||||
},
|
||||
request,
|
||||
}),
|
||||
})
|
||||
.await;
|
||||
rx.await
|
||||
.context("elicitation request channel closed unexpectedly")
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => true,
|
||||
AskForApproval::OnFailure => false,
|
||||
AskForApproval::OnRequest => false,
|
||||
AskForApproval::UnlessTrusted => false,
|
||||
AskForApproval::Granular(granular_config) => !granular_config.allows_mcp_elicitations(),
|
||||
}
|
||||
}
|
||||
|
||||
type ResponderMap = HashMap<(String, RequestId), oneshot::Sender<ElicitationResponse>>;
|
||||
|
||||
fn can_auto_accept_elicitation(elicitation: &CreateElicitationRequestParams) -> bool {
|
||||
match elicitation {
|
||||
CreateElicitationRequestParams::FormElicitationParams {
|
||||
requested_schema, ..
|
||||
} => {
|
||||
// Auto-accept confirm/approval elicitations without schema requirements.
|
||||
requested_schema.properties.is_empty()
|
||||
}
|
||||
CreateElicitationRequestParams::UrlElicitationParams { .. } => false,
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
pub use mcp_connection_manager::MCP_SANDBOX_STATE_META_CAPABILITY;
|
||||
pub use mcp_connection_manager::McpConnectionManager;
|
||||
pub use mcp_connection_manager::McpRuntimeEnvironment;
|
||||
pub use mcp_connection_manager::SandboxState;
|
||||
pub use mcp_connection_manager::ToolInfo;
|
||||
pub use connection_manager::McpConnectionManager;
|
||||
pub use rmcp_client::MCP_SANDBOX_STATE_META_CAPABILITY;
|
||||
pub use runtime::McpRuntimeEnvironment;
|
||||
pub use runtime::SandboxState;
|
||||
pub use tools::ToolInfo;
|
||||
|
||||
pub use mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
pub use mcp::McpConfig;
|
||||
pub use mcp::ToolPluginProvenance;
|
||||
|
||||
pub use mcp_connection_manager::CodexAppsToolsCacheKey;
|
||||
pub use mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
pub use codex_apps::CodexAppsToolsCacheKey;
|
||||
pub use codex_apps::codex_apps_tools_cache_key;
|
||||
|
||||
pub use mcp::configured_mcp_servers;
|
||||
pub use mcp::effective_mcp_servers;
|
||||
@@ -33,11 +33,15 @@ pub use mcp::oauth_login_support;
|
||||
pub use mcp::resolve_oauth_scopes;
|
||||
pub use mcp::should_retry_without_scopes;
|
||||
|
||||
pub use codex_apps::filter_non_codex_apps_mcp_tools_only;
|
||||
pub use mcp::mcp_permission_prompt_is_auto_approved;
|
||||
pub use mcp::qualified_mcp_tool_name_prefix;
|
||||
pub use mcp_connection_manager::declared_openai_file_input_param_names;
|
||||
pub use mcp_connection_manager::filter_non_codex_apps_mcp_tools_only;
|
||||
pub use tools::declared_openai_file_input_param_names;
|
||||
|
||||
pub(crate) mod codex_apps;
|
||||
pub(crate) mod connection_manager;
|
||||
pub(crate) mod elicitation;
|
||||
pub(crate) mod mcp;
|
||||
pub(crate) mod mcp_connection_manager;
|
||||
pub(crate) mod mcp_tool_names;
|
||||
pub(crate) mod rmcp_client;
|
||||
pub(crate) mod runtime;
|
||||
pub(crate) mod tools;
|
||||
|
||||
@@ -26,17 +26,17 @@ use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_protocol::mcp::Resource;
|
||||
use codex_protocol::mcp::ResourceTemplate;
|
||||
use codex_protocol::mcp::Tool;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::McpAuthStatus;
|
||||
use codex_protocol::protocol::McpListToolsResponseEvent;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_connection_manager::McpRuntimeEnvironment;
|
||||
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
|
||||
use crate::codex_apps::codex_apps_tools_cache_key;
|
||||
use crate::connection_manager::McpConnectionManager;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
|
||||
pub const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
|
||||
const MCP_TOOL_NAME_PREFIX: &str = "mcp";
|
||||
@@ -66,13 +66,18 @@ pub fn qualified_mcp_tool_name_prefix(server_name: &str) -> String {
|
||||
/// of being shown to the user.
|
||||
pub fn mcp_permission_prompt_is_auto_approved(
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> bool {
|
||||
approval_policy == AskForApproval::Never
|
||||
&& matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
)
|
||||
if approval_policy != AskForApproval::Never {
|
||||
return false;
|
||||
}
|
||||
|
||||
match permission_profile {
|
||||
PermissionProfile::Disabled | PermissionProfile::External { .. } => true,
|
||||
PermissionProfile::Managed { file_system, .. } => {
|
||||
file_system.to_sandbox_policy().has_full_disk_write_access()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP runtime settings derived from `codex_core::config::Config`.
|
||||
@@ -229,7 +234,7 @@ pub async fn read_mcp_resource(
|
||||
&config.approval_policy,
|
||||
String::new(),
|
||||
tx_event,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
PermissionProfile::default(),
|
||||
runtime_environment,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
@@ -294,7 +299,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
|
||||
&config.approval_policy,
|
||||
submit_id,
|
||||
tx_event,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
PermissionProfile::default(),
|
||||
runtime_environment,
|
||||
config.codex_home.clone(),
|
||||
codex_apps_tools_cache_key(auth),
|
||||
|
||||
@@ -3,6 +3,9 @@ use codex_config::Constrained;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
@@ -33,6 +36,35 @@ fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() {
|
||||
assert!(mcp_permission_prompt_is_auto_approved(
|
||||
AskForApproval::Never,
|
||||
&PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
network: NetworkSandboxPolicy::Enabled,
|
||||
},
|
||||
));
|
||||
assert!(mcp_permission_prompt_is_auto_approved(
|
||||
AskForApproval::Never,
|
||||
&PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
));
|
||||
assert!(!mcp_permission_prompt_is_auto_approved(
|
||||
AskForApproval::Never,
|
||||
&PermissionProfile::read_only(),
|
||||
));
|
||||
assert!(!mcp_permission_prompt_is_auto_approved(
|
||||
AskForApproval::OnRequest,
|
||||
&PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
network: NetworkSandboxPolicy::Enabled,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_plugin_provenance_collects_app_and_mcp_sources() {
|
||||
let provenance = ToolPluginProvenance::from_capability_summaries(&[
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,591 @@
|
||||
//! RMCP client lifecycle for MCP server connections.
|
||||
//!
|
||||
//! This module owns startup of individual RMCP clients: building the transport,
|
||||
//! initializing the server, listing raw tools, applying per-server tool filters,
|
||||
//! and exposing cached startup snapshots while a client is still connecting.
|
||||
//! Higher-level aggregation and resource/tool APIs live in
|
||||
//! [`crate::connection_manager`].
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::codex_apps::CachedCodexAppsToolsLoad;
|
||||
use crate::codex_apps::CodexAppsToolsCacheContext;
|
||||
use crate::codex_apps::filter_disallowed_codex_apps_tools;
|
||||
use crate::codex_apps::load_cached_codex_apps_tools;
|
||||
use crate::codex_apps::load_startup_cached_codex_apps_tools_snapshot;
|
||||
use crate::codex_apps::normalize_codex_apps_callable_name;
|
||||
use crate::codex_apps::normalize_codex_apps_callable_namespace;
|
||||
use crate::codex_apps::normalize_codex_apps_tool_title;
|
||||
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
|
||||
use crate::elicitation::ElicitationRequestManager;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp::ToolPluginProvenance;
|
||||
use crate::runtime::McpRuntimeEnvironment;
|
||||
use crate::runtime::emit_duration;
|
||||
use crate::tools::ToolFilter;
|
||||
use crate::tools::ToolInfo;
|
||||
use crate::tools::filter_tools;
|
||||
use crate::tools::tool_with_model_visible_input_schema;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use async_channel::Sender;
|
||||
use codex_api::SharedAuthProvider;
|
||||
use codex_async_utils::CancelErr;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_config::McpServerTransportConfig;
|
||||
use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_exec_server::HttpClient;
|
||||
use codex_exec_server::ReqwestHttpClient;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_rmcp_client::ExecutorStdioServerLauncher;
|
||||
use codex_rmcp_client::LocalStdioServerLauncher;
|
||||
use codex_rmcp_client::RmcpClient;
|
||||
use codex_rmcp_client::StdioServerLauncher;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::future::FutureExt;
|
||||
use futures::future::Shared;
|
||||
use rmcp::model::ClientCapabilities;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::FormElicitationCapability;
|
||||
use rmcp::model::Implementation;
|
||||
use rmcp::model::InitializeRequestParams;
|
||||
use rmcp::model::ProtocolVersion;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
/// MCP server capability indicating that Codex should include [`SandboxState`]
|
||||
/// in tool-call request `_meta` under this key.
|
||||
pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta";
|
||||
|
||||
pub(crate) const MCP_TOOLS_LIST_DURATION_METRIC: &str = "codex.mcp.tools.list.duration_ms";
|
||||
pub(crate) const MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC: &str =
|
||||
"codex.mcp.tools.fetch_uncached.duration_ms";
|
||||
pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub(crate) const DEFAULT_TOOL_TIMEOUT: Duration = Duration::from_secs(120);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ManagedClient {
|
||||
pub(crate) client: Arc<RmcpClient>,
|
||||
pub(crate) tools: Vec<ToolInfo>,
|
||||
pub(crate) tool_filter: ToolFilter,
|
||||
pub(crate) tool_timeout: Option<Duration>,
|
||||
pub(crate) server_instructions: Option<String>,
|
||||
pub(crate) server_supports_sandbox_state_meta_capability: bool,
|
||||
pub(crate) codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
}
|
||||
|
||||
impl ManagedClient {
|
||||
fn listed_tools(&self) -> Vec<ToolInfo> {
|
||||
let total_start = Instant::now();
|
||||
if let Some(cache_context) = self.codex_apps_tools_cache_context.as_ref()
|
||||
&& let CachedCodexAppsToolsLoad::Hit(tools) =
|
||||
load_cached_codex_apps_tools(cache_context)
|
||||
{
|
||||
emit_duration(
|
||||
MCP_TOOLS_LIST_DURATION_METRIC,
|
||||
total_start.elapsed(),
|
||||
&[("cache", "hit")],
|
||||
);
|
||||
return filter_tools(tools, &self.tool_filter);
|
||||
}
|
||||
|
||||
if self.codex_apps_tools_cache_context.is_some() {
|
||||
emit_duration(
|
||||
MCP_TOOLS_LIST_DURATION_METRIC,
|
||||
total_start.elapsed(),
|
||||
&[("cache", "miss")],
|
||||
);
|
||||
}
|
||||
|
||||
self.tools.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AsyncManagedClient {
|
||||
pub(crate) client: Shared<BoxFuture<'static, Result<ManagedClient, StartupOutcomeError>>>,
|
||||
pub(crate) startup_snapshot: Option<Vec<ToolInfo>>,
|
||||
pub(crate) startup_complete: Arc<AtomicBool>,
|
||||
pub(crate) tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
}
|
||||
|
||||
impl AsyncManagedClient {
|
||||
// Keep this constructor flat so the startup inputs remain readable at the
|
||||
// single call site instead of introducing a one-off params wrapper.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
server_name: String,
|
||||
config: McpServerConfig,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
cancel_token: CancellationToken,
|
||||
tx_event: Sender<Event>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
tool_plugin_provenance: Arc<ToolPluginProvenance>,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Self {
|
||||
let tool_filter = ToolFilter::from_config(&config);
|
||||
let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
)
|
||||
.map(|tools| filter_tools(tools, &tool_filter));
|
||||
let startup_tool_filter = tool_filter;
|
||||
let startup_complete = Arc::new(AtomicBool::new(false));
|
||||
let startup_complete_for_fut = Arc::clone(&startup_complete);
|
||||
let fut = async move {
|
||||
let outcome = async {
|
||||
if let Err(error) = validate_mcp_server_name(&server_name) {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
let client = Arc::new(
|
||||
make_rmcp_client(
|
||||
&server_name,
|
||||
config.clone(),
|
||||
store_mode,
|
||||
runtime_environment,
|
||||
runtime_auth_provider,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
match start_server_task(
|
||||
server_name,
|
||||
client,
|
||||
StartServerTaskParams {
|
||||
startup_timeout: config
|
||||
.startup_timeout_sec
|
||||
.or(Some(DEFAULT_STARTUP_TIMEOUT)),
|
||||
tool_timeout: config.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT),
|
||||
tool_filter: startup_tool_filter,
|
||||
tx_event,
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
},
|
||||
)
|
||||
.or_cancel(&cancel_token)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(CancelErr::Cancelled) => Err(StartupOutcomeError::Cancelled),
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
startup_complete_for_fut.store(true, Ordering::Release);
|
||||
outcome
|
||||
};
|
||||
let client = fut.boxed().shared();
|
||||
if startup_snapshot.is_some() {
|
||||
let startup_task = client.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = startup_task.await;
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
client,
|
||||
startup_snapshot,
|
||||
startup_complete,
|
||||
tool_plugin_provenance,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn client(&self) -> Result<ManagedClient, StartupOutcomeError> {
|
||||
self.client.clone().await
|
||||
}
|
||||
|
||||
fn startup_snapshot_while_initializing(&self) -> Option<Vec<ToolInfo>> {
|
||||
if !self.startup_complete.load(Ordering::Acquire) {
|
||||
return self.startup_snapshot.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn listed_tools(&self) -> Option<Vec<ToolInfo>> {
|
||||
let annotate_tools = |tools: Vec<ToolInfo>| {
|
||||
let mut tools = tools;
|
||||
for tool in &mut tools {
|
||||
if tool.server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
tool.tool = tool_with_model_visible_input_schema(&tool.tool);
|
||||
}
|
||||
|
||||
let plugin_names = match tool.connector_id.as_deref() {
|
||||
Some(connector_id) => self
|
||||
.tool_plugin_provenance
|
||||
.plugin_display_names_for_connector_id(connector_id),
|
||||
None => self
|
||||
.tool_plugin_provenance
|
||||
.plugin_display_names_for_mcp_server_name(tool.server_name.as_str()),
|
||||
};
|
||||
tool.plugin_display_names = plugin_names.to_vec();
|
||||
|
||||
if plugin_names.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let plugin_source_note = if plugin_names.len() == 1 {
|
||||
format!("This tool is part of plugin `{}`.", plugin_names[0])
|
||||
} else {
|
||||
format!(
|
||||
"This tool is part of plugins {}.",
|
||||
plugin_names
|
||||
.iter()
|
||||
.map(|plugin_name| format!("`{plugin_name}`"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let description = tool
|
||||
.tool
|
||||
.description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or("");
|
||||
let annotated_description = if description.is_empty() {
|
||||
plugin_source_note
|
||||
} else if matches!(description.chars().last(), Some('.' | '!' | '?')) {
|
||||
format!("{description} {plugin_source_note}")
|
||||
} else {
|
||||
format!("{description}. {plugin_source_note}")
|
||||
};
|
||||
tool.tool.description = Some(Cow::Owned(annotated_description));
|
||||
}
|
||||
tools
|
||||
};
|
||||
|
||||
// Keep cache payloads raw; plugin provenance is resolved per-session at read time.
|
||||
let tools = if let Some(startup_tools) = self.startup_snapshot_while_initializing() {
|
||||
Some(startup_tools)
|
||||
} else {
|
||||
match self.client().await {
|
||||
Ok(client) => Some(client.listed_tools()),
|
||||
Err(_) => self.startup_snapshot.clone(),
|
||||
}
|
||||
};
|
||||
tools.map(annotate_tools)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub(crate) enum StartupOutcomeError {
|
||||
#[error("MCP startup cancelled")]
|
||||
Cancelled,
|
||||
// We can't store the original error here because anyhow::Error doesn't implement
|
||||
// `Clone`.
|
||||
#[error("MCP startup failed: {error}")]
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for StartupOutcomeError {
|
||||
fn from(error: anyhow::Error) -> Self {
|
||||
Self::Failed {
|
||||
error: error.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn elicitation_capability_for_server(
|
||||
_server_name: &str,
|
||||
) -> Option<ElicitationCapability> {
|
||||
// https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities
|
||||
// indicates this should be an empty object.
|
||||
Some(ElicitationCapability {
|
||||
form: Some(FormElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
url: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn list_tools_for_client_uncached(
|
||||
server_name: &str,
|
||||
client: &Arc<RmcpClient>,
|
||||
timeout: Option<Duration>,
|
||||
server_instructions: Option<&str>,
|
||||
) -> Result<Vec<ToolInfo>> {
|
||||
let resp = client
|
||||
.list_tools_with_connector_ids(/*params*/ None, timeout)
|
||||
.await?;
|
||||
let tools = resp
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
let callable_name = normalize_codex_apps_callable_name(
|
||||
server_name,
|
||||
&tool.tool.name,
|
||||
tool.connector_id.as_deref(),
|
||||
tool.connector_name.as_deref(),
|
||||
);
|
||||
let callable_namespace = normalize_codex_apps_callable_namespace(
|
||||
server_name,
|
||||
tool.connector_name.as_deref(),
|
||||
);
|
||||
let connector_name = tool.connector_name;
|
||||
let connector_description = tool.connector_description;
|
||||
let mut tool_def = tool.tool;
|
||||
if let Some(title) = tool_def.title.as_deref() {
|
||||
let normalized_title =
|
||||
normalize_codex_apps_tool_title(server_name, connector_name.as_deref(), title);
|
||||
if tool_def.title.as_deref() != Some(normalized_title.as_str()) {
|
||||
tool_def.title = Some(normalized_title);
|
||||
}
|
||||
}
|
||||
ToolInfo {
|
||||
server_name: server_name.to_owned(),
|
||||
callable_name,
|
||||
callable_namespace,
|
||||
server_instructions: server_instructions.map(str::to_string),
|
||||
tool: tool_def,
|
||||
connector_id: tool.connector_id,
|
||||
connector_name,
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
return Ok(filter_disallowed_codex_apps_tools(tools));
|
||||
}
|
||||
Ok(tools)
|
||||
}
|
||||
|
||||
fn resolve_bearer_token(
|
||||
server_name: &str,
|
||||
bearer_token_env_var: Option<&str>,
|
||||
) -> Result<Option<String>> {
|
||||
let Some(env_var) = bearer_token_env_var else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match env::var(env_var) {
|
||||
Ok(value) => {
|
||||
if value.is_empty() {
|
||||
Err(anyhow!(
|
||||
"Environment variable {env_var} for MCP server '{server_name}' is empty"
|
||||
))
|
||||
} else {
|
||||
Ok(Some(value))
|
||||
}
|
||||
}
|
||||
Err(env::VarError::NotPresent) => Err(anyhow!(
|
||||
"Environment variable {env_var} for MCP server '{server_name}' is not set"
|
||||
)),
|
||||
Err(env::VarError::NotUnicode(_)) => Err(anyhow!(
|
||||
"Environment variable {env_var} for MCP server '{server_name}' contains invalid Unicode"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mcp_server_name(server_name: &str) -> Result<()> {
|
||||
let re = regex_lite::Regex::new(r"^[a-zA-Z0-9_-]+$")?;
|
||||
if !re.is_match(server_name) {
|
||||
return Err(anyhow!(
|
||||
"Invalid MCP server name '{server_name}': must match pattern {pattern}",
|
||||
pattern = re.as_str()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_server_task(
|
||||
server_name: String,
|
||||
client: Arc<RmcpClient>,
|
||||
params: StartServerTaskParams,
|
||||
) -> Result<ManagedClient, StartupOutcomeError> {
|
||||
let StartServerTaskParams {
|
||||
startup_timeout,
|
||||
tool_timeout,
|
||||
tool_filter,
|
||||
tx_event,
|
||||
elicitation_requests,
|
||||
codex_apps_tools_cache_context,
|
||||
} = params;
|
||||
let elicitation = elicitation_capability_for_server(&server_name);
|
||||
let params = InitializeRequestParams {
|
||||
meta: None,
|
||||
capabilities: ClientCapabilities {
|
||||
experimental: None,
|
||||
extensions: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation,
|
||||
tasks: None,
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-mcp-client".to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
title: Some("Codex".into()),
|
||||
description: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
};
|
||||
|
||||
let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event);
|
||||
|
||||
let initialize_result = client
|
||||
.initialize(params, startup_timeout, send_elicitation)
|
||||
.await
|
||||
.map_err(StartupOutcomeError::from)?;
|
||||
|
||||
let server_supports_sandbox_state_meta_capability = initialize_result
|
||||
.capabilities
|
||||
.experimental
|
||||
.as_ref()
|
||||
.and_then(|exp| exp.get(MCP_SANDBOX_STATE_META_CAPABILITY))
|
||||
.is_some();
|
||||
let list_start = Instant::now();
|
||||
let fetch_start = Instant::now();
|
||||
let tools = list_tools_for_client_uncached(
|
||||
&server_name,
|
||||
&client,
|
||||
startup_timeout,
|
||||
initialize_result.instructions.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(StartupOutcomeError::from)?;
|
||||
emit_duration(
|
||||
MCP_TOOLS_FETCH_UNCACHED_DURATION_METRIC,
|
||||
fetch_start.elapsed(),
|
||||
&[],
|
||||
);
|
||||
write_cached_codex_apps_tools_if_needed(
|
||||
&server_name,
|
||||
codex_apps_tools_cache_context.as_ref(),
|
||||
&tools,
|
||||
);
|
||||
if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
emit_duration(
|
||||
MCP_TOOLS_LIST_DURATION_METRIC,
|
||||
list_start.elapsed(),
|
||||
&[("cache", "miss")],
|
||||
);
|
||||
}
|
||||
let tools = filter_tools(tools, &tool_filter);
|
||||
|
||||
let managed = ManagedClient {
|
||||
client: Arc::clone(&client),
|
||||
tools,
|
||||
tool_timeout: Some(tool_timeout),
|
||||
tool_filter,
|
||||
server_instructions: initialize_result.instructions,
|
||||
server_supports_sandbox_state_meta_capability,
|
||||
codex_apps_tools_cache_context,
|
||||
};
|
||||
|
||||
Ok(managed)
|
||||
}
|
||||
|
||||
struct StartServerTaskParams {
|
||||
startup_timeout: Option<Duration>, // TODO: cancel_token should handle this.
|
||||
tool_timeout: Duration,
|
||||
tool_filter: ToolFilter,
|
||||
tx_event: Sender<Event>,
|
||||
elicitation_requests: ElicitationRequestManager,
|
||||
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
|
||||
}
|
||||
|
||||
async fn make_rmcp_client(
|
||||
server_name: &str,
|
||||
config: McpServerConfig,
|
||||
store_mode: OAuthCredentialsStoreMode,
|
||||
runtime_environment: McpRuntimeEnvironment,
|
||||
runtime_auth_provider: Option<SharedAuthProvider>,
|
||||
) -> Result<RmcpClient, StartupOutcomeError> {
|
||||
let McpServerConfig {
|
||||
transport,
|
||||
experimental_environment,
|
||||
..
|
||||
} = config;
|
||||
let remote_environment = match experimental_environment.as_deref() {
|
||||
None | Some("local") => false,
|
||||
Some("remote") => {
|
||||
if !runtime_environment.environment().is_remote() {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"remote MCP server `{server_name}` requires a remote environment"
|
||||
)));
|
||||
}
|
||||
true
|
||||
}
|
||||
Some(environment) => {
|
||||
return Err(StartupOutcomeError::from(anyhow!(
|
||||
"unsupported experimental_environment `{environment}` for MCP server `{server_name}`"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
match transport {
|
||||
McpServerTransportConfig::Stdio {
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
env_vars,
|
||||
cwd,
|
||||
} => {
|
||||
let command_os: OsString = command.into();
|
||||
let args_os: Vec<OsString> = args.into_iter().map(Into::into).collect();
|
||||
let env_os = env.map(|env| {
|
||||
env.into_iter()
|
||||
.map(|(key, value)| (key.into(), value.into()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
let launcher = if remote_environment {
|
||||
Arc::new(ExecutorStdioServerLauncher::new(
|
||||
runtime_environment.environment().get_exec_backend(),
|
||||
runtime_environment.fallback_cwd(),
|
||||
))
|
||||
} else {
|
||||
Arc::new(LocalStdioServerLauncher::new(
|
||||
runtime_environment.fallback_cwd(),
|
||||
)) as Arc<dyn StdioServerLauncher>
|
||||
};
|
||||
|
||||
// `RmcpClient` always sees a launched MCP stdio server. The
|
||||
// launcher hides whether that means a local child process or an
|
||||
// executor process whose stdin/stdout bytes cross the process API.
|
||||
RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd, launcher)
|
||||
.await
|
||||
.map_err(|err| StartupOutcomeError::from(anyhow!(err)))
|
||||
}
|
||||
McpServerTransportConfig::StreamableHttp {
|
||||
url,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
bearer_token_env_var,
|
||||
} => {
|
||||
let http_client: Arc<dyn HttpClient> = if remote_environment {
|
||||
runtime_environment.environment().get_http_client()
|
||||
} else {
|
||||
Arc::new(ReqwestHttpClient)
|
||||
};
|
||||
let resolved_bearer_token =
|
||||
match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) {
|
||||
Ok(token) => token,
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
RmcpClient::new_streamable_http_client(
|
||||
server_name,
|
||||
&url,
|
||||
resolved_bearer_token,
|
||||
http_headers,
|
||||
env_http_headers,
|
||||
store_mode,
|
||||
http_client,
|
||||
runtime_auth_provider,
|
||||
)
|
||||
.await
|
||||
.map_err(StartupOutcomeError::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Runtime support for Model Context Protocol (MCP) servers.
|
||||
//!
|
||||
//! This module contains data that describes the runtime environment in which MCP
|
||||
//! servers execute, plus the sandbox state payload sent to capable servers and a
|
||||
//! tiny shared metrics helper. Transport startup and orchestration live in
|
||||
//! [`crate::rmcp_client`] and [`crate::connection_manager`].
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server::Environment;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SandboxState {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub sandbox_cwd: PathBuf,
|
||||
#[serde(default)]
|
||||
pub use_legacy_landlock: bool,
|
||||
}
|
||||
|
||||
/// Runtime placement information used when starting MCP server transports.
|
||||
///
|
||||
/// `McpConfig` describes what servers exist. This value describes where those
|
||||
/// servers should run for the current caller. Keep it explicit at manager
|
||||
/// construction time so status/snapshot paths and real sessions make the same
|
||||
/// local-vs-remote decision. `fallback_cwd` is not a per-server override; it is
|
||||
/// used when a stdio server omits `cwd` and the launcher needs a concrete
|
||||
/// process working directory.
|
||||
#[derive(Clone)]
|
||||
pub struct McpRuntimeEnvironment {
|
||||
environment: Arc<Environment>,
|
||||
fallback_cwd: PathBuf,
|
||||
}
|
||||
|
||||
impl McpRuntimeEnvironment {
|
||||
pub fn new(environment: Arc<Environment>, fallback_cwd: PathBuf) -> Self {
|
||||
Self {
|
||||
environment,
|
||||
fallback_cwd,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn environment(&self) -> Arc<Environment> {
|
||||
Arc::clone(&self.environment)
|
||||
}
|
||||
|
||||
pub(crate) fn fallback_cwd(&self) -> PathBuf {
|
||||
self.fallback_cwd.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_duration(metric: &str, duration: Duration, tags: &[(&str, &str)]) {
|
||||
if let Some(metrics) = codex_otel::global() {
|
||||
let _ = metrics.record_duration(metric, duration, tags);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,134 @@
|
||||
//! Allocates model-visible MCP tool names while preserving raw MCP identities.
|
||||
//! MCP tool metadata, filtering, schema shaping, and name qualification.
|
||||
//!
|
||||
//! Raw MCP tool identities must be preserved for protocol calls, while
|
||||
//! model-visible tool names must be sanitized, deduplicated, and kept within API
|
||||
//! limits. This module owns that translation as well as the shared [`ToolInfo`]
|
||||
//! type and helpers that adjust tool schemas before exposing them to the model.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_config::McpServerConfig;
|
||||
use codex_protocol::ToolName;
|
||||
use rmcp::model::Tool;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value as JsonValue;
|
||||
use sha1::Digest;
|
||||
use sha1::Sha1;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::mcp::sanitize_responses_api_tool_name;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
|
||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
const CALLABLE_NAME_HASH_LEN: usize = 12;
|
||||
pub(crate) const MCP_TOOLS_CACHE_WRITE_DURATION_METRIC: &str =
|
||||
"codex.mcp.tools.cache_write.duration_ms";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolInfo {
|
||||
/// Raw MCP server name used for routing the tool call.
|
||||
pub server_name: String,
|
||||
/// Model-visible tool name used in Responses API tool declarations.
|
||||
#[serde(rename = "tool_name", alias = "callable_name")]
|
||||
pub callable_name: String,
|
||||
/// Model-visible namespace used for deferred tool loading.
|
||||
#[serde(rename = "tool_namespace", alias = "callable_namespace")]
|
||||
pub callable_namespace: String,
|
||||
/// Instructions from the MCP server initialize result.
|
||||
#[serde(default)]
|
||||
pub server_instructions: Option<String>,
|
||||
/// Raw MCP tool definition; `tool.name` is sent back to the MCP server.
|
||||
pub tool: Tool,
|
||||
pub connector_id: Option<String>,
|
||||
pub connector_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub plugin_display_names: Vec<String>,
|
||||
pub connector_description: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolInfo {
|
||||
pub fn canonical_tool_name(&self) -> ToolName {
|
||||
ToolName::namespaced(self.callable_namespace.clone(), self.callable_name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn declared_openai_file_input_param_names(
|
||||
meta: Option<&Map<String, JsonValue>>,
|
||||
) -> Vec<String> {
|
||||
let Some(meta) = meta else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
meta.get(META_OPENAI_FILE_PARAMS)
|
||||
.and_then(JsonValue::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(JsonValue::as_str)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A tool is allowed to be used if both are true:
|
||||
/// 1. enabled is None (no allowlist is set) or the tool is explicitly enabled.
|
||||
/// 2. The tool is not explicitly disabled.
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct ToolFilter {
|
||||
pub(crate) enabled: Option<HashSet<String>>,
|
||||
pub(crate) disabled: HashSet<String>,
|
||||
}
|
||||
|
||||
impl ToolFilter {
|
||||
pub(crate) fn from_config(cfg: &McpServerConfig) -> Self {
|
||||
let enabled = cfg
|
||||
.enabled_tools
|
||||
.as_ref()
|
||||
.map(|tools| tools.iter().cloned().collect::<HashSet<_>>());
|
||||
let disabled = cfg
|
||||
.disabled_tools
|
||||
.as_ref()
|
||||
.map(|tools| tools.iter().cloned().collect::<HashSet<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
Self { enabled, disabled }
|
||||
}
|
||||
|
||||
pub(crate) fn allows(&self, tool_name: &str) -> bool {
|
||||
if let Some(enabled) = &self.enabled
|
||||
&& !enabled.contains(tool_name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
!self.disabled.contains(tool_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the model-visible view of a tool while preserving the raw metadata
|
||||
/// used by execution. Keep cache entries raw and call this at manager return
|
||||
/// boundaries.
|
||||
pub(crate) fn tool_with_model_visible_input_schema(tool: &Tool) -> Tool {
|
||||
let file_params = declared_openai_file_input_param_names(tool.meta.as_deref());
|
||||
if file_params.is_empty() {
|
||||
return tool.clone();
|
||||
}
|
||||
|
||||
let mut tool = tool.clone();
|
||||
let mut input_schema = JsonValue::Object(tool.input_schema.as_ref().clone());
|
||||
mask_input_schema_for_file_path_params(&mut input_schema, &file_params);
|
||||
if let JsonValue::Object(input_schema) = input_schema {
|
||||
tool.input_schema = Arc::new(input_schema);
|
||||
}
|
||||
tool
|
||||
}
|
||||
|
||||
pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<ToolInfo> {
|
||||
tools
|
||||
.into_iter()
|
||||
.filter(|tool| filter.allows(&tool.tool.name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns a qualified-name lookup for MCP tools.
|
||||
///
|
||||
@@ -121,6 +237,57 @@ struct CallableToolCandidate {
|
||||
callable_name: String,
|
||||
}
|
||||
|
||||
const MCP_TOOL_NAME_DELIMITER: &str = "__";
|
||||
const MAX_TOOL_NAME_LENGTH: usize = 64;
|
||||
const CALLABLE_NAME_HASH_LEN: usize = 12;
|
||||
const META_OPENAI_FILE_PARAMS: &str = "openai/fileParams";
|
||||
|
||||
fn mask_input_schema_for_file_path_params(input_schema: &mut JsonValue, file_params: &[String]) {
|
||||
let Some(properties) = input_schema
|
||||
.as_object_mut()
|
||||
.and_then(|schema| schema.get_mut("properties"))
|
||||
.and_then(JsonValue::as_object_mut)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for field_name in file_params {
|
||||
let Some(property_schema) = properties.get_mut(field_name) else {
|
||||
continue;
|
||||
};
|
||||
mask_input_property_schema(property_schema);
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_input_property_schema(schema: &mut JsonValue) {
|
||||
let Some(object) = schema.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut description = object
|
||||
.get("description")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::to_string)
|
||||
.unwrap_or_default();
|
||||
let guidance = "This parameter expects an absolute local file path. If you want to upload a file, provide the absolute path to that file here.";
|
||||
if description.is_empty() {
|
||||
description = guidance.to_string();
|
||||
} else if !description.contains(guidance) {
|
||||
description = format!("{description} {guidance}");
|
||||
}
|
||||
|
||||
let is_array = object.get("type").and_then(JsonValue::as_str) == Some("array")
|
||||
|| object.get("items").is_some();
|
||||
object.clear();
|
||||
object.insert("description".to_string(), JsonValue::String(description));
|
||||
if is_array {
|
||||
object.insert("type".to_string(), JsonValue::String("array".to_string()));
|
||||
object.insert("items".to_string(), serde_json::json!({ "type": "string" }));
|
||||
} else {
|
||||
object.insert("type".to_string(), JsonValue::String("string".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
fn sha1_hex(s: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(s.as_bytes());
|
||||
@@ -14,14 +14,18 @@ workspace = true
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
futures = { workspace = true, features = ["alloc", "std"] }
|
||||
gethostname = { workspace = true }
|
||||
multimap = { workspace = true }
|
||||
@@ -44,8 +48,16 @@ wildmatch = { workspace = true }
|
||||
dns-lookup = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi-util = { workspace = true }
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
@@ -842,10 +842,10 @@ pub enum ResidencyRequirement {
|
||||
|
||||
impl ConfigRequirementsToml {
|
||||
pub fn apply_remote_sandbox_config(&mut self, hostname: Option<&str>) {
|
||||
let Some(hostname) = hostname.and_then(normalize_hostname) else {
|
||||
let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(remote_sandbox_config) = self.remote_sandbox_config.as_ref() else {
|
||||
let Some(hostname) = hostname.and_then(normalize_hostname) else {
|
||||
return;
|
||||
};
|
||||
let Some(matched_config) = remote_sandbox_config
|
||||
|
||||
@@ -7,6 +7,7 @@ mod fingerprint;
|
||||
mod hook_config;
|
||||
mod host_name;
|
||||
mod key_aliases;
|
||||
pub mod loader;
|
||||
mod marketplace_edit;
|
||||
mod mcp_edit;
|
||||
mod mcp_types;
|
||||
@@ -17,7 +18,6 @@ pub mod profile_toml;
|
||||
mod project_root_markers;
|
||||
mod requirements_exec_policy;
|
||||
pub mod schema;
|
||||
pub mod shell_environment;
|
||||
mod skills_config;
|
||||
mod state;
|
||||
mod thread_config;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# `codex-core` config loader
|
||||
# `codex-config` loader
|
||||
|
||||
This module is the canonical place to **load and describe Codex configuration layers** (user config, CLI/session overrides, managed config, and MDM-managed preferences) and to produce:
|
||||
|
||||
@@ -8,9 +8,9 @@ This module is the canonical place to **load and describe Codex configuration la
|
||||
|
||||
## Public surface
|
||||
|
||||
Exported from `codex_core::config_loader`:
|
||||
Exported from `codex_config::loader`:
|
||||
|
||||
- `load_config_layers_state(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements, thread_config_loader, host_name) -> ConfigLayerStack`
|
||||
- `load_config_layers_state(fs, codex_home, cwd_opt, cli_overrides, overrides, cloud_requirements, thread_config_loader) -> ConfigLayerStack`
|
||||
- `ConfigLayerStack`
|
||||
- `effective_config() -> toml::Value`
|
||||
- `origins() -> HashMap<String, ConfigLayerMetadata>`
|
||||
@@ -41,8 +41,10 @@ computing the effective config and origins metadata. This is what
|
||||
Most callers want the effective config plus metadata:
|
||||
|
||||
```rust
|
||||
use codex_core::config_loader::{CloudRequirementsLoader, LoaderOverrides, load_config_layers_state};
|
||||
use codex_config::NoopThreadConfigLoader;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
@@ -57,7 +59,6 @@ let layers = load_config_layers_state(
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
).await?;
|
||||
|
||||
let effective = layers.effective_config();
|
||||
+3
-3
@@ -1,10 +1,10 @@
|
||||
use super::LoaderOverrides;
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::macos::ManagedAdminConfigLayer;
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::macos::load_managed_admin_config_layer;
|
||||
use codex_config::config_error_from_toml;
|
||||
use codex_config::io_error_from_config_error;
|
||||
use crate::diagnostics::config_error_from_toml;
|
||||
use crate::diagnostics::io_error_from_config_error;
|
||||
use crate::state::LoaderOverrides;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::io;
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::ConfigRequirementsToml;
|
||||
use super::ConfigRequirementsWithSources;
|
||||
use super::RequirementSource;
|
||||
use super::merge_requirements_with_remote_sandbox_config;
|
||||
use crate::config_requirements::ConfigRequirementsToml;
|
||||
use crate::config_requirements::ConfigRequirementsWithSources;
|
||||
use crate::config_requirements::RequirementSource;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use core_foundation::base::TCFType;
|
||||
@@ -65,7 +65,6 @@ fn load_managed_admin_config() -> io::Result<Option<ManagedAdminConfigLayer>> {
|
||||
pub(crate) async fn load_managed_admin_requirements_toml(
|
||||
target: &mut ConfigRequirementsWithSources,
|
||||
override_base64: Option<&str>,
|
||||
host_name: Option<&str>,
|
||||
) -> io::Result<()> {
|
||||
if let Some(encoded) = override_base64 {
|
||||
let trimmed = encoded.trim();
|
||||
@@ -77,7 +76,6 @@ pub(crate) async fn load_managed_admin_requirements_toml(
|
||||
target,
|
||||
managed_preferences_requirements_source(),
|
||||
parse_managed_requirements_base64(trimmed)?,
|
||||
host_name,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -89,7 +87,6 @@ pub(crate) async fn load_managed_admin_requirements_toml(
|
||||
target,
|
||||
managed_preferences_requirements_source(),
|
||||
requirements,
|
||||
host_name,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -2,17 +2,29 @@ mod layer_io;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::config_loader::layer_io::LoadedConfigLayers;
|
||||
use self::layer_io::LoadedConfigLayers;
|
||||
use crate::CONFIG_TOML_FILE;
|
||||
use crate::cloud_requirements::CloudRequirementsLoader;
|
||||
use crate::config_requirements::ConfigRequirementsToml;
|
||||
use crate::config_requirements::ConfigRequirementsWithSources;
|
||||
use crate::config_requirements::RequirementSource;
|
||||
use crate::config_requirements::SandboxModeRequirement;
|
||||
use crate::config_toml::ConfigToml;
|
||||
use crate::config_toml::ProjectConfig;
|
||||
use crate::diagnostics::ConfigError;
|
||||
use crate::diagnostics::config_error_from_toml;
|
||||
use crate::diagnostics::first_layer_config_error_from_entries as typed_first_layer_config_error_from_entries;
|
||||
use crate::diagnostics::io_error_from_config_error;
|
||||
use crate::merge::merge_toml_values;
|
||||
use crate::overrides::build_cli_overrides_layer;
|
||||
use crate::project_root_markers::default_project_root_markers;
|
||||
use crate::project_root_markers::project_root_markers_from_config;
|
||||
use crate::state::ConfigLayerEntry;
|
||||
use crate::state::ConfigLayerStack;
|
||||
use crate::state::LoaderOverrides;
|
||||
use crate::thread_config::ThreadConfigContext;
|
||||
use crate::thread_config::ThreadConfigLoader;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigRequirementsWithSources;
|
||||
use codex_config::ThreadConfigContext;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
@@ -29,71 +41,14 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
pub use codex_config::AppRequirementToml;
|
||||
pub use codex_config::AppsRequirementsToml;
|
||||
pub use codex_config::CloudRequirementsLoadError;
|
||||
pub use codex_config::CloudRequirementsLoadErrorCode;
|
||||
pub use codex_config::CloudRequirementsLoader;
|
||||
pub use codex_config::ConfigError;
|
||||
pub use codex_config::ConfigLayerEntry;
|
||||
pub use codex_config::ConfigLayerStack;
|
||||
pub use codex_config::ConfigLayerStackOrdering;
|
||||
pub use codex_config::ConfigLoadError;
|
||||
pub use codex_config::ConfigRequirements;
|
||||
pub use codex_config::ConfigRequirementsToml;
|
||||
pub use codex_config::ConstrainedWithSource;
|
||||
pub use codex_config::FeatureRequirementsToml;
|
||||
pub use codex_config::FilesystemConstraints;
|
||||
pub use codex_config::FilesystemDenyReadPattern;
|
||||
pub use codex_config::HookEventsToml;
|
||||
pub use codex_config::HookHandlerConfig;
|
||||
pub use codex_config::LoaderOverrides;
|
||||
pub use codex_config::ManagedHooksRequirementsToml;
|
||||
pub use codex_config::MatcherGroup;
|
||||
pub use codex_config::McpServerIdentity;
|
||||
pub use codex_config::McpServerRequirement;
|
||||
pub use codex_config::NetworkConstraints;
|
||||
pub use codex_config::NetworkDomainPermissionToml;
|
||||
pub use codex_config::NetworkDomainPermissionsToml;
|
||||
pub use codex_config::NetworkRequirementsToml;
|
||||
pub use codex_config::NetworkUnixSocketPermissionToml;
|
||||
pub use codex_config::NetworkUnixSocketPermissionsToml;
|
||||
pub use codex_config::RemoteSandboxConfigToml;
|
||||
pub use codex_config::RequirementSource;
|
||||
pub use codex_config::ResidencyRequirement;
|
||||
pub use codex_config::SandboxModeRequirement;
|
||||
pub use codex_config::Sourced;
|
||||
pub use codex_config::TextPosition;
|
||||
pub use codex_config::TextRange;
|
||||
pub use codex_config::WebSearchModeRequirement;
|
||||
pub(crate) use codex_config::build_cli_overrides_layer;
|
||||
pub(crate) use codex_config::config_error_from_toml;
|
||||
pub use codex_config::default_project_root_markers;
|
||||
pub use codex_config::format_config_error;
|
||||
pub use codex_config::format_config_error_with_source;
|
||||
pub(crate) use codex_config::io_error_from_config_error;
|
||||
pub use codex_config::merge_toml_values;
|
||||
pub use codex_config::project_root_markers_from_config;
|
||||
#[cfg(test)]
|
||||
pub(crate) use codex_config::version_for_toml;
|
||||
|
||||
/// On Unix systems, load default settings from this file path, if present.
|
||||
/// Note that /etc/codex/ is treated as a "config folder," so subfolders such
|
||||
/// as skills/ and rules/ will also be honored.
|
||||
pub const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml";
|
||||
#[cfg(unix)]
|
||||
const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml";
|
||||
|
||||
#[cfg(windows)]
|
||||
const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData";
|
||||
|
||||
pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option<ConfigError> {
|
||||
codex_config::first_layer_config_error::<ConfigToml>(layers, CONFIG_TOML_FILE).await
|
||||
}
|
||||
|
||||
pub(crate) async fn first_layer_config_error_from_entries(
|
||||
layers: &[ConfigLayerEntry],
|
||||
) -> Option<ConfigError> {
|
||||
codex_config::first_layer_config_error_from_entries::<ConfigToml>(layers, CONFIG_TOML_FILE)
|
||||
.await
|
||||
async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> Option<ConfigError> {
|
||||
typed_first_layer_config_error_from_entries::<ConfigToml>(layers, CONFIG_TOML_FILE).await
|
||||
}
|
||||
|
||||
/// To build up the set of admin-enforced constraints, we build up from multiple
|
||||
@@ -136,7 +91,6 @@ pub async fn load_config_layers_state(
|
||||
overrides: LoaderOverrides,
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
thread_config_loader: &dyn ThreadConfigLoader,
|
||||
host_name: Option<&str>,
|
||||
) -> io::Result<ConfigLayerStack> {
|
||||
let ignore_user_config = overrides.ignore_user_config;
|
||||
let ignore_user_and_project_exec_policy_rules =
|
||||
@@ -148,7 +102,6 @@ pub async fn load_config_layers_state(
|
||||
&mut config_requirements_toml,
|
||||
RequirementSource::CloudRequirements,
|
||||
requirements,
|
||||
host_name,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,28 +111,20 @@ pub async fn load_config_layers_state(
|
||||
overrides
|
||||
.macos_managed_config_requirements_base64
|
||||
.as_deref(),
|
||||
host_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Honor the system requirements.toml location.
|
||||
let requirements_toml_file = system_requirements_toml_file()?;
|
||||
load_requirements_toml(
|
||||
fs,
|
||||
&mut config_requirements_toml,
|
||||
&requirements_toml_file,
|
||||
host_name,
|
||||
)
|
||||
.await?;
|
||||
let requirements_toml_file = system_requirements_toml_file_with_overrides(&overrides)?;
|
||||
load_requirements_toml(fs, &mut config_requirements_toml, &requirements_toml_file).await?;
|
||||
|
||||
// Make a best-effort to support the legacy `managed_config.toml` as a
|
||||
// requirements specification.
|
||||
let loaded_config_layers =
|
||||
layer_io::load_config_layers_internal(fs, codex_home, overrides).await?;
|
||||
layer_io::load_config_layers_internal(fs, codex_home, overrides.clone()).await?;
|
||||
load_requirements_from_legacy_scheme(
|
||||
&mut config_requirements_toml,
|
||||
loaded_config_layers.clone(),
|
||||
host_name,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -210,7 +155,7 @@ pub async fn load_config_layers_state(
|
||||
|
||||
// Include an entry for the "system" config folder, loading its config.toml,
|
||||
// if it exists.
|
||||
let system_config_toml_file = system_config_toml_file()?;
|
||||
let system_config_toml_file = system_config_toml_file_with_overrides(&overrides)?;
|
||||
let system_layer =
|
||||
load_config_toml_for_required_layer(fs, &system_config_toml_file, |config_toml| {
|
||||
ConfigLayerEntry::new(
|
||||
@@ -428,11 +373,11 @@ async fn load_config_toml_for_required_layer(
|
||||
/// If available, apply requirements from the platform system
|
||||
/// `requirements.toml` location to `config_requirements_toml` by filling in
|
||||
/// any unset fields.
|
||||
async fn load_requirements_toml(
|
||||
#[doc(hidden)]
|
||||
pub async fn load_requirements_toml(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
config_requirements_toml: &mut ConfigRequirementsWithSources,
|
||||
requirements_toml_file: &AbsolutePathBuf,
|
||||
host_name: Option<&str>,
|
||||
) -> io::Result<()> {
|
||||
match fs
|
||||
.read_file_text(requirements_toml_file, /*sandbox*/ None)
|
||||
@@ -465,7 +410,6 @@ async fn load_requirements_toml(
|
||||
file: requirements_toml_file.clone(),
|
||||
},
|
||||
requirements_config,
|
||||
host_name,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -494,16 +438,34 @@ fn system_requirements_toml_file() -> io::Result<AbsolutePathBuf> {
|
||||
windows_system_requirements_toml_file()
|
||||
}
|
||||
|
||||
fn system_requirements_toml_file_with_overrides(
|
||||
overrides: &LoaderOverrides,
|
||||
) -> io::Result<AbsolutePathBuf> {
|
||||
match &overrides.system_requirements_path {
|
||||
Some(path) => AbsolutePathBuf::from_absolute_path(path),
|
||||
None => system_requirements_toml_file(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn system_config_toml_file() -> io::Result<AbsolutePathBuf> {
|
||||
pub fn system_config_toml_file() -> io::Result<AbsolutePathBuf> {
|
||||
AbsolutePathBuf::from_absolute_path(Path::new(SYSTEM_CONFIG_TOML_FILE_UNIX))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn system_config_toml_file() -> io::Result<AbsolutePathBuf> {
|
||||
pub fn system_config_toml_file() -> io::Result<AbsolutePathBuf> {
|
||||
windows_system_config_toml_file()
|
||||
}
|
||||
|
||||
fn system_config_toml_file_with_overrides(
|
||||
overrides: &LoaderOverrides,
|
||||
) -> io::Result<AbsolutePathBuf> {
|
||||
match &overrides.system_config_path {
|
||||
Some(path) => AbsolutePathBuf::from_absolute_path(path),
|
||||
None => system_config_toml_file(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_codex_system_dir() -> PathBuf {
|
||||
let program_data = windows_program_data_dir_from_known_folder().unwrap_or_else(|err| {
|
||||
@@ -580,7 +542,6 @@ fn windows_program_data_dir_from_known_folder() -> io::Result<PathBuf> {
|
||||
async fn load_requirements_from_legacy_scheme(
|
||||
config_requirements_toml: &mut ConfigRequirementsWithSources,
|
||||
loaded_config_layers: LoadedConfigLayers,
|
||||
host_name: Option<&str>,
|
||||
) -> io::Result<()> {
|
||||
// In this implementation, earlier layers cannot be overwritten by later
|
||||
// layers, so list managed_config_from_mdm first because it has the highest
|
||||
@@ -617,7 +578,6 @@ async fn load_requirements_from_legacy_scheme(
|
||||
config_requirements_toml,
|
||||
source,
|
||||
ConfigRequirementsToml::from(legacy_config),
|
||||
host_name,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -628,9 +588,11 @@ pub(super) fn merge_requirements_with_remote_sandbox_config(
|
||||
target: &mut ConfigRequirementsWithSources,
|
||||
source: RequirementSource,
|
||||
mut requirements: ConfigRequirementsToml,
|
||||
host_name: Option<&str>,
|
||||
) {
|
||||
requirements.apply_remote_sandbox_config(host_name);
|
||||
if requirements.remote_sandbox_config.is_some() {
|
||||
let host_name = crate::host_name();
|
||||
requirements.apply_remote_sandbox_config(host_name.as_deref());
|
||||
}
|
||||
target.merge_unset_fields(source, requirements);
|
||||
}
|
||||
|
||||
@@ -844,7 +806,8 @@ fn project_trust_for_lookup_key(
|
||||
///
|
||||
/// This ensures that multiple config layers can be merged together correctly
|
||||
/// even if they were loaded from different directories.
|
||||
pub(crate) fn resolve_relative_paths_in_config_toml(
|
||||
#[doc(hidden)]
|
||||
pub fn resolve_relative_paths_in_config_toml(
|
||||
value_from_config_toml: TomlValue,
|
||||
base_dir: &Path,
|
||||
) -> io::Result<TomlValue> {
|
||||
@@ -18,6 +18,8 @@ use toml::Value as TomlValue;
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LoaderOverrides {
|
||||
pub managed_config_path: Option<PathBuf>,
|
||||
pub system_config_path: Option<PathBuf>,
|
||||
pub system_requirements_path: Option<PathBuf>,
|
||||
pub ignore_user_config: bool,
|
||||
pub ignore_user_and_project_exec_policy_rules: bool,
|
||||
//TODO(gt): Add a macos_ prefix to this field and remove the target_os check.
|
||||
@@ -31,11 +33,17 @@ impl LoaderOverrides {
|
||||
///
|
||||
/// This is intended for tests that should load only repo-controlled config fixtures.
|
||||
pub fn without_managed_config_for_tests() -> Self {
|
||||
Self::with_managed_config_path_for_tests(
|
||||
std::env::temp_dir()
|
||||
.join("codex-config-tests")
|
||||
.join("managed_config.toml"),
|
||||
)
|
||||
let base = std::env::temp_dir().join("codex-config-tests");
|
||||
Self {
|
||||
managed_config_path: Some(base.join("managed_config.toml")),
|
||||
system_config_path: Some(base.join("config.toml")),
|
||||
system_requirements_path: Some(base.join("requirements.toml")),
|
||||
ignore_user_config: false,
|
||||
ignore_user_and_project_exec_policy_rules: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
managed_preferences_base64: Some(String::new()),
|
||||
macos_managed_config_requirements_base64: Some(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns overrides with host MDM disabled and managed config loaded from `managed_config_path`.
|
||||
@@ -44,11 +52,7 @@ impl LoaderOverrides {
|
||||
pub fn with_managed_config_path_for_tests(managed_config_path: PathBuf) -> Self {
|
||||
Self {
|
||||
managed_config_path: Some(managed_config_path),
|
||||
ignore_user_config: false,
|
||||
ignore_user_and_project_exec_policy_rules: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
managed_preferences_base64: Some(String::new()),
|
||||
macos_managed_config_requirements_base64: Some(String::new()),
|
||||
..Self::without_managed_config_for_tests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,17 @@ pub use crate::mcp_types::McpServerTransportConfig;
|
||||
pub use crate::mcp_types::RawMcpServerConfig;
|
||||
pub use codex_protocol::config_types::AltScreenMode;
|
||||
pub use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
pub use codex_protocol::config_types::ModeKind;
|
||||
pub use codex_protocol::config_types::Personality;
|
||||
pub use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
pub use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use wildmatch::WildMatchPattern;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@@ -707,21 +709,6 @@ impl From<SandboxWorkspaceWrite> for codex_app_server_protocol::SandboxSettings
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ShellEnvironmentPolicyInherit {
|
||||
/// "Core" environment variables for the platform. On UNIX, this would
|
||||
/// include HOME, LOGNAME, PATH, SHELL, and USER, among others.
|
||||
Core,
|
||||
|
||||
/// Inherits the full environment from the parent process.
|
||||
#[default]
|
||||
All,
|
||||
|
||||
/// Do not inherit any environment variables from the parent process.
|
||||
None,
|
||||
}
|
||||
|
||||
/// Policy for building the `env` when spawning a process via either the
|
||||
/// `shell` or `local_shell` tool.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
|
||||
@@ -742,37 +729,6 @@ pub struct ShellEnvironmentPolicyToml {
|
||||
pub experimental_use_profile: Option<bool>,
|
||||
}
|
||||
|
||||
pub type EnvironmentVariablePattern = WildMatchPattern<'*', '?'>;
|
||||
|
||||
/// Deriving the `env` based on this policy works as follows:
|
||||
/// 1. Create an initial map based on the `inherit` policy.
|
||||
/// 2. If `ignore_default_excludes` is false, filter the map using the default
|
||||
/// exclude pattern(s), which are: `"*KEY*"`, `"*SECRET*"`, and `"*TOKEN*"`.
|
||||
/// 3. If `exclude` is not empty, filter the map using the provided patterns.
|
||||
/// 4. Insert any entries from `r#set` into the map.
|
||||
/// 5. If non-empty, filter the map using the `include_only` patterns.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ShellEnvironmentPolicy {
|
||||
/// Starting point when building the environment.
|
||||
pub inherit: ShellEnvironmentPolicyInherit,
|
||||
|
||||
/// True to skip the check to exclude default environment variables that
|
||||
/// contain "KEY", "SECRET", or "TOKEN" in their name. Defaults to true.
|
||||
pub ignore_default_excludes: bool,
|
||||
|
||||
/// Environment variable names to exclude from the environment.
|
||||
pub exclude: Vec<EnvironmentVariablePattern>,
|
||||
|
||||
/// (key, value) pairs to insert in the environment.
|
||||
pub r#set: HashMap<String, String>,
|
||||
|
||||
/// Environment variable names to retain in the environment.
|
||||
pub include_only: Vec<EnvironmentVariablePattern>,
|
||||
|
||||
/// If true, the shell profile will be used to run the command.
|
||||
pub use_profile: bool,
|
||||
}
|
||||
|
||||
impl From<ShellEnvironmentPolicyToml> for ShellEnvironmentPolicy {
|
||||
fn from(toml: ShellEnvironmentPolicyToml) -> Self {
|
||||
// Default to inheriting the full environment when not specified.
|
||||
@@ -804,19 +760,6 @@ impl From<ShellEnvironmentPolicyToml> for ShellEnvironmentPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ShellEnvironmentPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inherit: ShellEnvironmentPolicyInherit::All,
|
||||
ignore_default_excludes: true,
|
||||
exclude: Vec::new(),
|
||||
r#set: HashMap::new(),
|
||||
include_only: Vec::new(),
|
||||
use_profile: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "types_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -46,7 +46,7 @@ codex_rust_crate(
|
||||
"//:AGENTS.md",
|
||||
],
|
||||
test_shard_counts = {
|
||||
"core-all-test": 8,
|
||||
"core-all-test": 16,
|
||||
"core-unit-tests": 8,
|
||||
},
|
||||
test_tags = ["no-sandbox"],
|
||||
|
||||
@@ -120,9 +120,6 @@ uuid = { workspace = true, features = ["serde", "v4", "v5"] }
|
||||
which = { workspace = true }
|
||||
whoami = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
|
||||
# Build OpenSSL from source for musl builds.
|
||||
[target.x86_64-unknown-linux-musl.dependencies]
|
||||
openssl-sys = { workspace = true, features = ["vendored"] }
|
||||
@@ -131,13 +128,6 @@ openssl-sys = { workspace = true, features = ["vendored"] }
|
||||
[target.aarch64-unknown-linux-musl.dependencies]
|
||||
openssl-sys = { workspace = true, features = ["vendored"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell",
|
||||
] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
codex-shell-escalation = { workspace = true }
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ fn assistant_message(text: &str, phase: Option<MessagePhase>) -> ResponseItem {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase,
|
||||
}
|
||||
}
|
||||
@@ -519,7 +518,6 @@ async fn append_message_records_assistant_message() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: message.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
)
|
||||
@@ -678,7 +676,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "parent seed context".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
|
||||
|
||||
@@ -11,13 +11,13 @@ use crate::config::Config;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::agent_roles::parse_agent_role_file_contents;
|
||||
use crate::config::deserialize_config_toml_with_base;
|
||||
use crate::config_loader::ConfigLayerEntry;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::resolve_relative_paths_in_config_toml;
|
||||
use anyhow::anyhow;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::loader::resolve_relative_paths_in_config_toml;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
@@ -2,9 +2,9 @@ use super::*;
|
||||
use crate::SkillsManager;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::skills_load_input_from_config;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -574,7 +574,7 @@ writable_roots = ["./sandbox-root"]
|
||||
false
|
||||
);
|
||||
|
||||
match &*config.permissions.sandbox_policy {
|
||||
match &config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::WorkspaceWrite { network_access, .. } => {
|
||||
assert_eq!(*network_access, true);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
//! 3. We do **not** walk past the project root.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::default_project_root_markers;
|
||||
use crate::config_loader::merge_toml_values;
|
||||
use crate::config_loader::project_root_markers_from_config;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::default_project_root_markers;
|
||||
use codex_config::merge_toml_values;
|
||||
use codex_config::project_root_markers_from_config;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_features::Feature;
|
||||
|
||||
@@ -38,7 +38,7 @@ pub(crate) async fn apply_patch(
|
||||
match assess_patch_safety(
|
||||
&action,
|
||||
turn_context.approval_policy.value(),
|
||||
turn_context.sandbox_policy.get(),
|
||||
&turn_context.permission_profile(),
|
||||
file_system_sandbox_policy,
|
||||
&turn_context.cwd,
|
||||
turn_context.windows_sandbox_level,
|
||||
|
||||
@@ -65,7 +65,6 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "first request".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}],
|
||||
&turn_context,
|
||||
@@ -94,7 +93,6 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "commentary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: Some(MessagePhase::Commentary),
|
||||
}],
|
||||
&turn_context,
|
||||
@@ -108,7 +106,6 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "final response".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
}],
|
||||
&turn_context,
|
||||
@@ -122,7 +119,6 @@ async fn build_arc_monitor_request_includes_relevant_history_and_null_policies()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "latest request".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}],
|
||||
&turn_context,
|
||||
@@ -277,7 +273,6 @@ async fn monitor_action_posts_expected_arc_request() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "please run the tool".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}],
|
||||
&turn_context,
|
||||
@@ -358,7 +353,6 @@ async fn monitor_action_uses_env_url_and_token_overrides() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "please run the tool".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}],
|
||||
&turn_context,
|
||||
@@ -428,7 +422,6 @@ async fn monitor_action_rejects_legacy_response_fields() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "please run the tool".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}],
|
||||
&turn_context,
|
||||
|
||||
@@ -278,7 +278,6 @@ impl CodexThread {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText { text: message }],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
let pending_item = match pending_message_input_item(&message) {
|
||||
|
||||
@@ -509,7 +509,6 @@ fn build_compacted_history_with_limit(
|
||||
content: vec![ContentItem::InputText {
|
||||
text: message.clone(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
}
|
||||
@@ -524,7 +523,6 @@ fn build_compacted_history_with_limit(
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText { text: summary_text }],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ fn collect_user_messages_extracts_user_text_only() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "ignored".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -72,7 +71,6 @@ fn collect_user_messages_extracts_user_text_only() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "first".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Other,
|
||||
@@ -97,7 +95,6 @@ do things
|
||||
</INSTRUCTIONS>"#
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -106,7 +103,6 @@ do things
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<ENVIRONMENT_CONTEXT>cwd=/tmp</ENVIRONMENT_CONTEXT>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -115,7 +111,6 @@ do things
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "real user message".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -223,7 +218,6 @@ async fn process_compacted_history_replaces_developer_messages() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "stale permissions".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -232,7 +226,6 @@ async fn process_compacted_history_replaces_developer_messages() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -241,7 +234,6 @@ async fn process_compacted_history_replaces_developer_messages() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "stale personality".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -256,7 +248,6 @@ async fn process_compacted_history_replaces_developer_messages() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
assert_eq!(refreshed, expected);
|
||||
@@ -270,7 +261,6 @@ async fn process_compacted_history_reinjects_full_initial_context() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}];
|
||||
let (refreshed, mut expected) = process_compacted_history_with_test_session(
|
||||
@@ -284,7 +274,6 @@ async fn process_compacted_history_reinjects_full_initial_context() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
assert_eq!(refreshed, expected);
|
||||
@@ -304,7 +293,6 @@ keep me updated
|
||||
</INSTRUCTIONS>"#
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -317,7 +305,6 @@ keep me updated
|
||||
</environment_context>"#
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -330,7 +317,6 @@ keep me updated
|
||||
</turn_aborted>"#
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -339,7 +325,6 @@ keep me updated
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -348,7 +333,6 @@ keep me updated
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "stale developer instructions".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -363,7 +347,6 @@ keep me updated
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
assert_eq!(refreshed, expected);
|
||||
@@ -378,7 +361,6 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "older user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -387,7 +369,6 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!("{SUMMARY_PREFIX}\nsummary text"),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -396,7 +377,6 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "latest user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -413,7 +393,6 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "older user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -422,7 +401,6 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!("{SUMMARY_PREFIX}\nsummary text"),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -433,7 +411,6 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "latest user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
assert_eq!(refreshed, expected);
|
||||
@@ -447,7 +424,6 @@ async fn process_compacted_history_reinjects_model_switch_message() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}];
|
||||
let previous_turn_settings = PreviousTurnSettings {
|
||||
@@ -477,7 +453,6 @@ async fn process_compacted_history_reinjects_model_switch_message() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "summary".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
});
|
||||
assert_eq!(refreshed, expected);
|
||||
@@ -492,7 +467,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "older user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -501,7 +475,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "latest user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -510,7 +483,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!("{SUMMARY_PREFIX}\nsummary text"),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -520,7 +492,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "fresh permissions".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}];
|
||||
|
||||
@@ -533,7 +504,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "older user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -542,7 +512,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "fresh permissions".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -551,7 +520,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "latest user".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -560,7 +528,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!("{SUMMARY_PREFIX}\nsummary text"),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -578,7 +545,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "fresh permissions".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}];
|
||||
|
||||
@@ -591,7 +557,6 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "fresh permissions".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Compaction {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::AgentRoleConfig;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::config_toml::AgentRoleToml;
|
||||
use codex_config::config_toml::AgentsToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
|
||||
+67
-180
@@ -1,29 +1,34 @@
|
||||
use super::LoaderOverrides;
|
||||
use super::load_config_layers_state;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::ConstraintError;
|
||||
use crate::config_loader::CloudRequirementsLoadError;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerEntry;
|
||||
use crate::config_loader::ConfigLoadError;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConfigRequirementsWithSources;
|
||||
use crate::config_loader::FilesystemDenyReadPattern;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::load_requirements_toml;
|
||||
use crate::config_loader::version_for_toml;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::CloudRequirementsLoadError;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigError;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigLoadError;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ConfigRequirementsWithSources;
|
||||
use codex_config::FilesystemDenyReadPattern;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::SessionThreadConfig;
|
||||
use codex_config::StaticThreadConfigLoader;
|
||||
use codex_config::ThreadConfigSource;
|
||||
use codex_config::config_error_from_toml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::loader::load_requirements_toml;
|
||||
use codex_config::version_for_toml;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -33,7 +38,7 @@ use std::path::Path;
|
||||
use tempfile::tempdir;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
fn config_error_from_io(err: &std::io::Error) -> &super::ConfigError {
|
||||
fn config_error_from_io(err: &std::io::Error) -> &ConfigError {
|
||||
err.get_ref()
|
||||
.and_then(|err| err.downcast_ref::<ConfigLoadError>())
|
||||
.map(ConfigLoadError::config_error)
|
||||
@@ -103,15 +108,13 @@ async fn returns_config_error_for_invalid_user_config_toml() {
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("expected error");
|
||||
|
||||
let config_error = config_error_from_io(&err);
|
||||
let expected_toml_error = toml::from_str::<TomlValue>(contents).expect_err("parse error");
|
||||
let expected_config_error =
|
||||
super::config_error_from_toml(&config_path, contents, expected_toml_error);
|
||||
let expected_config_error = config_error_from_toml(&config_path, contents, expected_toml_error);
|
||||
assert_eq!(config_error, &expected_config_error);
|
||||
}
|
||||
|
||||
@@ -136,7 +139,6 @@ async fn ignore_user_config_keeps_empty_user_layer() -> std::io::Result<()> {
|
||||
},
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -168,7 +170,6 @@ async fn ignore_rules_marks_config_stack_for_exec_policy_rule_skip() -> std::io:
|
||||
},
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -194,7 +195,6 @@ async fn returns_config_error_for_invalid_managed_config_toml() {
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("expected error");
|
||||
@@ -202,7 +202,7 @@ async fn returns_config_error_for_invalid_managed_config_toml() {
|
||||
let config_error = config_error_from_io(&err);
|
||||
let expected_toml_error = toml::from_str::<TomlValue>(contents).expect_err("parse error");
|
||||
let expected_config_error =
|
||||
super::config_error_from_toml(&managed_path, contents, expected_toml_error);
|
||||
config_error_from_toml(&managed_path, contents, expected_toml_error);
|
||||
assert_eq!(config_error, &expected_config_error);
|
||||
}
|
||||
|
||||
@@ -281,7 +281,6 @@ extra = true
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
@@ -316,7 +315,6 @@ async fn returns_empty_when_all_layers_missing() {
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load layers");
|
||||
@@ -325,7 +323,7 @@ async fn returns_empty_when_all_layers_missing() {
|
||||
.expect("expected a user layer even when CODEX_HOME/config.toml does not exist");
|
||||
assert_eq!(
|
||||
&ConfigLayerEntry {
|
||||
name: super::ConfigLayerSource::User {
|
||||
name: ConfigLayerSource::User {
|
||||
file: AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, tmp.path())
|
||||
},
|
||||
config: TomlValue::Table(toml::map::Map::new()),
|
||||
@@ -350,7 +348,7 @@ async fn returns_empty_when_all_layers_missing() {
|
||||
let num_system_layers = layers
|
||||
.layers_high_to_low()
|
||||
.iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::System { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::System { .. }))
|
||||
.count();
|
||||
assert_eq!(
|
||||
num_system_layers, 1,
|
||||
@@ -374,18 +372,24 @@ async fn includes_thread_config_layers_in_stack() -> anyhow::Result<()> {
|
||||
let cwd_dir = tmp.path().join("project");
|
||||
tokio::fs::create_dir_all(&cwd_dir).await?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(&cwd_dir)?;
|
||||
let overrides = LoaderOverrides::without_managed_config_for_tests();
|
||||
let expected_system_config = AbsolutePathBuf::from_absolute_path(
|
||||
overrides
|
||||
.system_config_path
|
||||
.as_ref()
|
||||
.expect("test overrides should include a system config path"),
|
||||
)?;
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
tmp.path(),
|
||||
Some(cwd),
|
||||
&[("features.plugins".to_string(), TomlValue::Boolean(true))],
|
||||
LoaderOverrides::without_managed_config_for_tests(),
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&StaticThreadConfigLoader::new(vec![ThreadConfigSource::Session(SessionThreadConfig {
|
||||
features: BTreeMap::from([("plugins".to_string(), false)]),
|
||||
..Default::default()
|
||||
})]),
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -397,13 +401,13 @@ async fn includes_thread_config_layers_in_stack() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
layer_sources,
|
||||
vec![
|
||||
super::ConfigLayerSource::SessionFlags,
|
||||
super::ConfigLayerSource::SessionFlags,
|
||||
super::ConfigLayerSource::User {
|
||||
ConfigLayerSource::SessionFlags,
|
||||
ConfigLayerSource::SessionFlags,
|
||||
ConfigLayerSource::User {
|
||||
file: AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, tmp.path()),
|
||||
},
|
||||
super::ConfigLayerSource::System {
|
||||
file: super::system_config_toml_file()?,
|
||||
ConfigLayerSource::System {
|
||||
file: expected_system_config,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -462,7 +466,6 @@ flag = false
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("load config");
|
||||
@@ -482,7 +485,7 @@ flag = false
|
||||
.find(|layer| {
|
||||
matches!(
|
||||
layer.name,
|
||||
super::ConfigLayerSource::LegacyManagedConfigTomlFromMdm
|
||||
ConfigLayerSource::LegacyManagedConfigTomlFromMdm
|
||||
)
|
||||
})
|
||||
.expect("mdm layer");
|
||||
@@ -523,7 +526,7 @@ writable_roots = ["~/code"]
|
||||
.await?;
|
||||
|
||||
let expected_root = AbsolutePathBuf::from_absolute_path(home.join("code"))?;
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
match &config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
assert_eq!(
|
||||
writable_roots
|
||||
@@ -566,7 +569,6 @@ allowed_sandbox_modes = ["read-only"]
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -629,7 +631,6 @@ allowed_approval_policies = ["never"]
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -671,7 +672,6 @@ personality = true
|
||||
LOCAL_FS.as_ref(),
|
||||
&mut config_requirements_toml,
|
||||
&requirements_file,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -687,14 +687,14 @@ personality = true
|
||||
.allowed_web_search_modes
|
||||
.as_deref()
|
||||
.cloned(),
|
||||
Some(vec![crate::config_loader::WebSearchModeRequirement::Cached])
|
||||
Some(vec![codex_config::WebSearchModeRequirement::Cached])
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements_toml
|
||||
.feature_requirements
|
||||
.as_ref()
|
||||
.map(|requirements| requirements.value.clone()),
|
||||
Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("personality".to_string(), true)]),
|
||||
})
|
||||
);
|
||||
@@ -733,14 +733,14 @@ personality = true
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements.enforce_residency.value(),
|
||||
Some(crate::config_loader::ResidencyRequirement::Us)
|
||||
Some(codex_config::ResidencyRequirement::Us)
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements
|
||||
.feature_requirements
|
||||
.as_ref()
|
||||
.map(|requirements| requirements.value.clone()),
|
||||
Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("personality".to_string(), true)]),
|
||||
})
|
||||
);
|
||||
@@ -787,7 +787,6 @@ allowed_approval_policies = ["on-request"]
|
||||
}))
|
||||
}),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -847,7 +846,6 @@ allowed_approval_policies = ["on-request"]
|
||||
LOCAL_FS.as_ref(),
|
||||
&mut config_requirements_toml,
|
||||
&AbsolutePathBuf::try_from(requirements_file)?,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -869,54 +867,6 @@ allowed_approval_policies = ["on-request"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn system_remote_sandbox_config_keeps_cloud_sandbox_modes() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let requirements_file = tmp.path().join("requirements.toml");
|
||||
tokio::fs::write(
|
||||
&requirements_file,
|
||||
r#"
|
||||
[[remote_sandbox_config]]
|
||||
hostname_patterns = ["runner-*.ci.example.com"]
|
||||
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cloud_source = RequirementSource::CloudRequirements;
|
||||
let mut config_requirements_toml = ConfigRequirementsWithSources::default();
|
||||
config_requirements_toml.merge_unset_fields(
|
||||
cloud_source.clone(),
|
||||
toml::from_str(
|
||||
r#"
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
"#,
|
||||
)?,
|
||||
);
|
||||
load_requirements_toml(
|
||||
LOCAL_FS.as_ref(),
|
||||
&mut config_requirements_toml,
|
||||
&AbsolutePathBuf::try_from(requirements_file)?,
|
||||
Some("runner-01.ci.example.com"),
|
||||
)
|
||||
.await?;
|
||||
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
|
||||
|
||||
assert_eq!(
|
||||
config_requirements
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::new_workspace_write_policy()),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: "WorkspaceWrite".into(),
|
||||
allowed: "[ReadOnly]".into(),
|
||||
requirement_source: cloud_source,
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn load_requirements_toml_resolves_deny_read_against_parent() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
@@ -938,7 +888,6 @@ deny_read = ["./sensitive", "../shared/secret.txt"]
|
||||
LOCAL_FS.as_ref(),
|
||||
&mut config_requirements_toml,
|
||||
&requirements_file,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -993,7 +942,6 @@ deny_read = ["./sensitive/**/*.txt"]
|
||||
LOCAL_FS.as_ref(),
|
||||
&mut config_requirements_toml,
|
||||
&requirements_file,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1062,7 +1010,6 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
LoaderOverrides::default(),
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1125,7 +1072,6 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result
|
||||
LoaderOverrides::default(),
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1142,53 +1088,6 @@ async fn load_config_layers_includes_cloud_hook_requirements() -> anyhow::Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let codex_home = tmp.path().join("home");
|
||||
tokio::fs::create_dir_all(&codex_home).await?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?;
|
||||
|
||||
let requirements: ConfigRequirementsToml = toml::from_str(
|
||||
r#"
|
||||
allowed_sandbox_modes = ["read-only"]
|
||||
|
||||
[[remote_sandbox_config]]
|
||||
hostname_patterns = ["runner-*.ci.example.com"]
|
||||
allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
"#,
|
||||
)?;
|
||||
let cloud_requirements = CloudRequirementsLoader::new(async move { Ok(Some(requirements)) });
|
||||
let layers = load_config_layers_state(
|
||||
LOCAL_FS.as_ref(),
|
||||
&codex_home,
|
||||
Some(cwd),
|
||||
&[] as &[(String, TomlValue)],
|
||||
LoaderOverrides::default(),
|
||||
cloud_requirements,
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
Some("runner-01.ci.example.com"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
layers.requirements_toml().allowed_sandbox_modes,
|
||||
Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
crate::config_loader::SandboxModeRequirement::WorkspaceWrite,
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
layers
|
||||
.requirements()
|
||||
.sandbox_policy
|
||||
.can_set(&SandboxPolicy::new_workspace_write_policy())
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
@@ -1210,7 +1109,6 @@ async fn load_config_layers_fails_when_cloud_requirements_loader_fails() -> anyh
|
||||
))
|
||||
}),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await
|
||||
.expect_err("cloud requirements failure should fail closed");
|
||||
@@ -1259,7 +1157,6 @@ async fn project_layers_prefer_closest_cwd() -> std::io::Result<()> {
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1267,7 +1164,7 @@ async fn project_layers_prefer_closest_cwd() -> std::io::Result<()> {
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.filter_map(|layer| match &layer.name {
|
||||
super::ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -1406,18 +1303,17 @@ async fn project_layer_is_added_when_dot_codex_exists_without_config_toml() -> s
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_layers: Vec<_> = layers
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
vec![&ConfigLayerEntry {
|
||||
name: super::ConfigLayerSource::Project {
|
||||
name: ConfigLayerSource::Project {
|
||||
dot_codex_folder: AbsolutePathBuf::from_absolute_path(project_root.join(".codex"))?,
|
||||
},
|
||||
config: TomlValue::Table(toml::map::Map::new()),
|
||||
@@ -1448,17 +1344,16 @@ async fn codex_home_is_not_loaded_as_project_layer_from_home_dir() -> std::io::R
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_layers: Vec<_> = layers
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
let expected: Vec<&ConfigLayerEntry> = Vec::new();
|
||||
assert_eq!(expected, project_layers);
|
||||
@@ -1507,23 +1402,22 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_layers: Vec<_> = layers
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
|
||||
let child_config: TomlValue = toml::from_str("foo = \"child\"\n").expect("parse child config");
|
||||
assert_eq!(
|
||||
vec![&ConfigLayerEntry {
|
||||
name: super::ConfigLayerSource::Project {
|
||||
name: ConfigLayerSource::Project {
|
||||
dot_codex_folder: AbsolutePathBuf::from_absolute_path(&nested_dot_codex)?,
|
||||
},
|
||||
config: child_config.clone(),
|
||||
@@ -1580,16 +1474,15 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
let project_layers_untrusted: Vec<_> = layers_untrusted
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
assert_eq!(project_layers_untrusted.len(), 1);
|
||||
assert!(
|
||||
@@ -1621,16 +1514,15 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result<
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
let project_layers_unknown: Vec<_> = layers_unknown
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
assert_eq!(project_layers_unknown.len(), 1);
|
||||
assert!(
|
||||
@@ -1689,17 +1581,16 @@ async fn project_trust_does_not_match_configured_alias_for_canonical_cwd() -> st
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let project_layers: Vec<_> = layers
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
assert_eq!(project_layers.len(), 1);
|
||||
assert!(
|
||||
@@ -1844,16 +1735,15 @@ async fn invalid_project_config_ignored_when_untrusted_or_unknown() -> std::io::
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
let project_layers: Vec<_> = layers
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
project_layers.len(),
|
||||
@@ -1914,16 +1804,15 @@ async fn project_layer_without_config_toml_is_disabled_when_untrusted_or_unknown
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
let project_layers: Vec<_> = layers
|
||||
.get_layers(
|
||||
super::ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. }))
|
||||
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
project_layers.len(),
|
||||
@@ -1976,7 +1865,6 @@ async fn cli_overrides_with_relative_paths_do_not_break_trust_check() -> std::io
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2021,7 +1909,6 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2029,7 +1916,7 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
|
||||
.layers_high_to_low()
|
||||
.into_iter()
|
||||
.filter_map(|layer| match &layer.name {
|
||||
super::ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder),
|
||||
ConfigLayerSource::Project { dot_codex_folder } => Some(dot_codex_folder),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
@@ -2051,14 +1938,14 @@ async fn project_root_markers_supports_alternate_markers() -> std::io::Result<()
|
||||
}
|
||||
|
||||
mod requirements_exec_policy_tests {
|
||||
use crate::config_loader::ConfigLayerEntry;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConfigRequirementsWithSources;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::exec_policy::load_exec_policy;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ConfigRequirementsWithSources;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::RequirementsExecPolicyDecisionToml;
|
||||
use codex_config::RequirementsExecPolicyParseError;
|
||||
use codex_config::RequirementsExecPolicyPatternTokenToml;
|
||||
@@ -4,11 +4,10 @@ use crate::config::ThreadStoreConfig;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::edit::apply_blocking;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::project_trust_key;
|
||||
use crate::plugins::PluginsManager;
|
||||
use assert_matches::assert_matches;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::config_toml::AgentRoleToml;
|
||||
use codex_config::config_toml::AgentsToml;
|
||||
use codex_config::config_toml::AutoReviewToml;
|
||||
@@ -21,6 +20,7 @@ use codex_config::config_toml::RealtimeTransport;
|
||||
use codex_config::config_toml::RealtimeWsMode;
|
||||
use codex_config::config_toml::RealtimeWsVersion;
|
||||
use codex_config::config_toml::ToolsToml;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::permissions_toml::FilesystemPermissionToml;
|
||||
use codex_config::permissions_toml::FilesystemPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionToml;
|
||||
@@ -56,13 +56,16 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
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::NetworkAccess;
|
||||
use codex_protocol::protocol::RealtimeVoice;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use serde::Deserialize;
|
||||
@@ -774,7 +777,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
|
||||
let memories_root = codex_home.path().join("memories").abs();
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
@@ -803,7 +806,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
]),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![memories_root],
|
||||
network_access: false,
|
||||
@@ -812,7 +815,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
Ok(())
|
||||
@@ -837,12 +840,151 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io:
|
||||
|
||||
assert_eq!(config.permissions.permission_profile(), permission_profile);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::DangerFullAccess
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_preserves_managed_unrestricted_filesystem()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let permission_profile = PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
permission_profile: Some(permission_profile.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.permissions.permission_profile(), permission_profile);
|
||||
assert_eq!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_unrestricted_permission_profile_still_enables_network_requirements()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let permission_profile = PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
network: NetworkSandboxPolicy::Enabled,
|
||||
};
|
||||
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
permission_profile: Some(permission_profile),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
"the legacy projection is intentionally lossy for managed unrestricted profiles"
|
||||
);
|
||||
|
||||
let layers = config
|
||||
.config_layer_stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut requirements = config.config_layer_stack.requirements().clone();
|
||||
requirements.network = Some(Sourced::new(
|
||||
codex_config::NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
));
|
||||
let mut requirements_toml = config.config_layer_stack.requirements_toml().clone();
|
||||
requirements_toml.network = Some(codex_config::NetworkRequirementsToml {
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
config.config_layer_stack = ConfigLayerStack::new(layers, requirements, requirements_toml)
|
||||
.expect("config layer stack with network requirements");
|
||||
|
||||
assert!(config.managed_network_requirements_enabled());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_applies_runtime_roots_to_legacy_projection()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
permission_profile: Some(permission_profile),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let memories_root = codex_home.path().join("memories").abs();
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(memories_root.as_path(), cwd.path())
|
||||
);
|
||||
assert_eq!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![memories_root],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_preserves_configured_network_proxy() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -941,7 +1083,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.file_system_sandbox_policy()
|
||||
.glob_scan_max_depth,
|
||||
Some(2)
|
||||
);
|
||||
@@ -951,7 +1093,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.file_system_sandbox_policy()
|
||||
.entries
|
||||
.contains(&FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
@@ -963,7 +1105,7 @@ async fn project_root_glob_none_compiles_to_filesystem_pattern_entry() -> std::i
|
||||
assert!(
|
||||
!config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.file_system_sandbox_policy()
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| matches!(
|
||||
@@ -1020,13 +1162,16 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io::Result<()> {
|
||||
async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
let external_write_path = if cfg!(windows) { r"C:\temp" } else { "/tmp" };
|
||||
let external_write_dir = TempDir::new()?;
|
||||
let external_write_path =
|
||||
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(external_write_dir.path())?)?;
|
||||
|
||||
let err = Config::load_from_base_config_with_overrides(
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
@@ -1036,7 +1181,7 @@ async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io:
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
external_write_path.to_string(),
|
||||
external_write_path.to_string_lossy().into_owned(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Write),
|
||||
)]),
|
||||
}),
|
||||
@@ -1052,14 +1197,25 @@ async fn permissions_profiles_reject_writes_outside_workspace_root() -> std::io:
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await
|
||||
.expect_err("writes outside the workspace root should be rejected");
|
||||
.await?;
|
||||
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(
|
||||
codex_home.path().join("memories"),
|
||||
)?)?;
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("filesystem writes outside the workspace root"),
|
||||
"{err}"
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(external_write_path.as_path(), cwd.path())
|
||||
);
|
||||
assert_eq!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![external_write_path, memories_root],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1149,7 +1305,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::unknown(
|
||||
@@ -1161,7 +1317,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<(
|
||||
}]),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
}
|
||||
@@ -1195,7 +1351,7 @@ async fn permissions_profiles_allow_unknown_special_paths_with_nested_entries()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::unknown(":future_special_path", Some("docs".into())),
|
||||
@@ -1222,11 +1378,11 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
FileSystemSandboxPolicy::restricted(Vec::new())
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
}
|
||||
@@ -1253,7 +1409,7 @@ async fn permissions_profiles_allow_empty_filesystem_with_warning() -> std::io::
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
FileSystemSandboxPolicy::restricted(Vec::new())
|
||||
);
|
||||
assert!(
|
||||
@@ -1350,16 +1506,10 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()>
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
config.permissions.network_sandbox_policy.is_enabled(),
|
||||
config.permissions.network_sandbox_policy().is_enabled(),
|
||||
"expected network sandbox policy to be enabled",
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.sandbox_policy
|
||||
.get()
|
||||
.has_full_network_access()
|
||||
);
|
||||
assert!(config.legacy_sandbox_policy().has_full_network_access());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1643,22 +1793,22 @@ exclude_slash_tmp = true
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sandbox_policy = config.permissions.sandbox_policy.get();
|
||||
let sandbox_policy = &config.legacy_sandbox_policy();
|
||||
assert_eq!(
|
||||
config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.file_system_sandbox_policy(),
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()),
|
||||
"case `{name}` should preserve filesystem semantics from legacy config"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
"case `{name}` should preserve network semantics from legacy config"
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(config.permissions.network_sandbox_policy, cwd.path())
|
||||
.file_system_sandbox_policy()
|
||||
.to_legacy_sandbox_policy(config.permissions.network_sandbox_policy(), cwd.path())
|
||||
.unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")),
|
||||
sandbox_policy.clone(),
|
||||
"case `{name}` should round-trip through split policies without drift"
|
||||
@@ -1826,12 +1976,12 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
|
||||
|
||||
let expected_backend = backend.abs();
|
||||
if cfg!(target_os = "windows") {
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
match &config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::ReadOnly { .. } => {}
|
||||
other => panic!("expected read-only policy on Windows, got {other:?}"),
|
||||
}
|
||||
} else {
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
match &config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
assert_eq!(
|
||||
writable_roots
|
||||
@@ -1889,7 +2039,7 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result
|
||||
.await?;
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
match &config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::ReadOnly { .. } => {}
|
||||
other => panic!("expected read-only policy on Windows, got {other:?}"),
|
||||
}
|
||||
@@ -1900,7 +2050,7 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result
|
||||
memories_root.display()
|
||||
);
|
||||
let expected_memories_root = memories_root.abs();
|
||||
match config.permissions.sandbox_policy.get() {
|
||||
match &config.legacy_sandbox_policy() {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
assert_eq!(
|
||||
writable_roots
|
||||
@@ -2103,24 +2253,25 @@ fn web_search_mode_disabled_overrides_legacy_request() {
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_uses_preference_for_read_only() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
|
||||
let mode =
|
||||
resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::new_read_only_policy());
|
||||
let permission_profile =
|
||||
PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_read_only_policy());
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &permission_profile);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_prefers_live_for_danger_full_access() {
|
||||
fn web_search_mode_for_turn_prefers_live_for_disabled_permissions() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Live);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_mode_for_turn_respects_disabled_for_danger_full_access() {
|
||||
fn web_search_mode_for_turn_respects_disabled_for_disabled_permissions() {
|
||||
let web_search_mode = Constrained::allow_any(WebSearchMode::Disabled);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Disabled);
|
||||
}
|
||||
@@ -2140,7 +2291,7 @@ fn web_search_mode_for_turn_falls_back_when_live_is_disallowed() -> anyhow::Resu
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess);
|
||||
let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled);
|
||||
|
||||
assert_eq!(mode, WebSearchMode::Cached);
|
||||
Ok(())
|
||||
@@ -2218,7 +2369,7 @@ async fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> {
|
||||
.await?;
|
||||
|
||||
assert!(matches!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
&SandboxPolicy::DangerFullAccess
|
||||
));
|
||||
|
||||
@@ -2252,12 +2403,12 @@ async fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::R
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(matches!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::ReadOnly { .. }
|
||||
));
|
||||
} else {
|
||||
assert!(matches!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
));
|
||||
}
|
||||
@@ -2381,7 +2532,6 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
let cfg =
|
||||
@@ -2517,7 +2667,6 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
|
||||
overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -3826,7 +3975,7 @@ async fn load_config_uses_requirements_guardian_policy_config() -> std::io::Resu
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
crate::config_loader::ConfigRequirementsToml {
|
||||
codex_config::ConfigRequirementsToml {
|
||||
guardian_policy_config: Some(
|
||||
" Use the workspace-managed guardian policy. ".to_string(),
|
||||
),
|
||||
@@ -3907,7 +4056,7 @@ async fn requirements_guardian_policy_beats_auto_review() -> std::io::Result<()>
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
crate::config_loader::ConfigRequirementsToml {
|
||||
codex_config::ConfigRequirementsToml {
|
||||
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -3971,7 +4120,7 @@ async fn load_config_ignores_empty_requirements_guardian_policy_config() -> std:
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
crate::config_loader::ConfigRequirementsToml {
|
||||
codex_config::ConfigRequirementsToml {
|
||||
guardian_policy_config: Some(" ".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -4103,15 +4252,15 @@ config_file = "./agents/researcher.toml"
|
||||
"#,
|
||||
)
|
||||
.expect("agent role layer config should parse");
|
||||
let config_layer_stack = crate::config_loader::ConfigLayerStack::new(
|
||||
vec![crate::config_loader::ConfigLayerEntry::new(
|
||||
let config_layer_stack = codex_config::ConfigLayerStack::new(
|
||||
vec![codex_config::ConfigLayerEntry::new(
|
||||
codex_app_server_protocol::ConfigLayerSource::User {
|
||||
file: codex_home.path().join(CONFIG_TOML_FILE).abs(),
|
||||
},
|
||||
layer_config,
|
||||
)],
|
||||
Default::default(),
|
||||
crate::config_loader::ConfigRequirementsToml::default(),
|
||||
codex_config::ConfigRequirementsToml::default(),
|
||||
)
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
@@ -5292,11 +5441,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::Never),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -5489,11 +5634,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_custom_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -5640,11 +5781,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -5776,11 +5913,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
permissions: Permissions {
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()),
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy::from(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
|
||||
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
|
||||
network: None,
|
||||
allow_login_shell: true,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -5892,14 +6025,12 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
{
|
||||
let fixture = create_test_fixture()?;
|
||||
|
||||
let requirements_toml = crate::config_loader::ConfigRequirementsToml {
|
||||
let requirements_toml = codex_config::ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: None,
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(vec![
|
||||
crate::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
@@ -5910,7 +6041,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
permissions: None,
|
||||
guardian_policy_config: None,
|
||||
};
|
||||
let requirement_source = crate::config_loader::RequirementSource::Unknown;
|
||||
let requirement_source = codex_config::RequirementSource::Unknown;
|
||||
let requirement_source_for_error = requirement_source.clone();
|
||||
let allowed = vec![WebSearchMode::Disabled, WebSearchMode::Cached];
|
||||
let constrained = Constrained::new(WebSearchMode::Cached, move |candidate| {
|
||||
@@ -5925,15 +6056,15 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
||||
})
|
||||
}
|
||||
})?;
|
||||
let requirements = crate::config_loader::ConfigRequirements {
|
||||
web_search_mode: crate::config_loader::ConstrainedWithSource::new(
|
||||
let requirements = codex_config::ConfigRequirements {
|
||||
web_search_mode: codex_config::ConstrainedWithSource::new(
|
||||
constrained,
|
||||
Some(requirement_source),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
let config_layer_stack =
|
||||
crate::config_loader::ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
codex_config::ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||
.expect("config layer stack");
|
||||
|
||||
let config = Config::load_config_with_layer_stack(
|
||||
@@ -6519,7 +6650,7 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow:
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
matches!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::ReadOnly { .. }
|
||||
),
|
||||
"Expected ReadOnly on Windows"
|
||||
@@ -6527,7 +6658,7 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow:
|
||||
} else {
|
||||
assert!(
|
||||
matches!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
),
|
||||
"Expected WorkspaceWrite sandbox for untrusted project"
|
||||
@@ -6545,17 +6676,15 @@ async fn requirements_disallowing_default_sandbox_falls_back_to_required_default
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
*config.permissions.sandbox_policy.get(),
|
||||
config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::new_read_only_policy()
|
||||
);
|
||||
Ok(())
|
||||
@@ -6570,10 +6699,10 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let requirements = crate::config_loader::ConfigRequirementsToml {
|
||||
let requirements = codex_config::ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_approvals_reviewers: None,
|
||||
allowed_sandbox_modes: Some(vec![crate::config_loader::SandboxModeRequirement::ReadOnly]),
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
@@ -6596,12 +6725,99 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
||||
.build()
|
||||
.await?;
|
||||
assert_eq!(
|
||||
*config.permissions.sandbox_policy.get(),
|
||||
config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::new_read_only_policy()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_falls_back_when_disallowed_by_requirements()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let requirements = codex_config::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.harness_overrides(ConfigOverrides {
|
||||
permission_profile: Some(PermissionProfile::Disabled),
|
||||
..Default::default()
|
||||
})
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async move {
|
||||
Ok(Some(requirements))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let expected_sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
assert_eq!(config.legacy_sandbox_policy(), expected_sandbox_policy);
|
||||
assert_eq!(
|
||||
config.permissions.permission_profile(),
|
||||
PermissionProfile::read_only()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permission_profile_override_preserves_split_write_roots() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = codex_home.path().join("workspace");
|
||||
let outside_root = codex_home.path().join("outside-write");
|
||||
std::fs::create_dir_all(&cwd)?;
|
||||
std::fs::create_dir_all(&outside_root)?;
|
||||
let outside_root =
|
||||
AbsolutePathBuf::from_absolute_path(outside_root).expect("outside root is absolute");
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: outside_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::Managed,
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(cwd))
|
||||
.harness_overrides(ConfigOverrides {
|
||||
permission_profile: Some(permission_profile),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
config
|
||||
.permissions
|
||||
.file_system_sandbox_policy()
|
||||
.can_write_path_with_cwd(outside_root.as_path(), config.cwd.as_path())
|
||||
);
|
||||
assert!(matches!(
|
||||
&config.legacy_sandbox_policy(),
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
));
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_web_search_mode_overrides_danger_full_access_default() -> std::io::Result<()>
|
||||
{
|
||||
@@ -6616,9 +6832,9 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() ->
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_web_search_modes: Some(vec![
|
||||
crate::config_loader::WebSearchModeRequirement::Cached,
|
||||
codex_config::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -6630,7 +6846,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() ->
|
||||
assert_eq!(
|
||||
resolve_web_search_mode_for_turn(
|
||||
&config.web_search_mode,
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&config.permissions.permission_profile(),
|
||||
),
|
||||
WebSearchMode::Cached,
|
||||
);
|
||||
@@ -6657,7 +6873,7 @@ trust_level = "untrusted"
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(workspace.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -6686,7 +6902,7 @@ async fn explicit_approval_policy_falls_back_when_disallowed_by_requirements() -
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -6707,8 +6923,8 @@ async fn feature_requirements_normalize_effective_feature_values() -> std::io::R
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([
|
||||
("personality".to_string(), true),
|
||||
("shell_tool".to_string(), false),
|
||||
@@ -6741,8 +6957,8 @@ async fn feature_requirements_auto_review_disables_guardian_approval() -> std::i
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("auto_review".to_string(), false)]),
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -6763,8 +6979,8 @@ async fn browser_feature_requirements_are_valid() -> std::io::Result<()> {
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([
|
||||
("in_app_browser".to_string(), false),
|
||||
("browser_use".to_string(), false),
|
||||
@@ -6798,8 +7014,8 @@ shell_tool = true
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([
|
||||
("personality".to_string(), true),
|
||||
("shell_tool".to_string(), false),
|
||||
@@ -6945,7 +7161,7 @@ async fn requirements_disallowing_default_approvals_reviewer_falls_back_to_requi
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -6971,7 +7187,7 @@ async fn root_approvals_reviewer_falls_back_when_disallowed_by_requirements() ->
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -7008,7 +7224,7 @@ approvals_reviewer = "user"
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::AutoReview]),
|
||||
..Default::default()
|
||||
}))
|
||||
@@ -7034,7 +7250,7 @@ async fn approvals_reviewer_preserves_valid_user_choice_when_allowed_by_requirem
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
allowed_approvals_reviewers: Some(vec![
|
||||
ApprovalsReviewer::User,
|
||||
ApprovalsReviewer::AutoReview,
|
||||
@@ -7186,8 +7402,8 @@ async fn feature_requirements_normalize_runtime_feature_mutations() -> std::io::
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([
|
||||
("personality".to_string(), true),
|
||||
("shell_tool".to_string(), false),
|
||||
@@ -7222,8 +7438,8 @@ async fn feature_requirements_warn_on_collab_legacy_alias() -> std::io::Result<(
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("collab".to_string(), true)]),
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -7252,8 +7468,8 @@ async fn feature_requirements_warn_and_ignore_unknown_feature() -> std::io::Resu
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(crate::config_loader::ConfigRequirementsToml {
|
||||
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("made_up_feature".to_string(), true)]),
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
+212
-89
@@ -1,20 +1,6 @@
|
||||
use crate::agents_md::AgentsMdManager;
|
||||
use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::ConstrainedWithSource;
|
||||
use crate::config_loader::FeatureRequirementsToml;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::config_loader::McpServerIdentity;
|
||||
use crate::config_loader::McpServerRequirement;
|
||||
use crate::config_loader::ResidencyRequirement;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::config_loader::load_config_layers_state;
|
||||
use crate::config_loader::project_trust_key;
|
||||
use crate::memories::memory_root;
|
||||
use crate::path_utils::normalize_for_native_workdir;
|
||||
use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS;
|
||||
@@ -22,6 +8,18 @@ use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::ConstrainedWithSource;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::McpServerIdentity;
|
||||
use codex_config::McpServerRequirement;
|
||||
use codex_config::ResidencyRequirement;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
@@ -29,6 +27,8 @@ use codex_config::config_toml::RealtimeAudioConfig;
|
||||
use codex_config::config_toml::RealtimeConfig;
|
||||
use codex_config::config_toml::ThreadStoreToml;
|
||||
use codex_config::config_toml::validate_model_providers;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::loader::project_trust_key;
|
||||
use codex_config::profile_toml::ConfigProfile;
|
||||
use codex_config::types::ApprovalsReviewer;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
@@ -44,7 +44,6 @@ use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_config::types::OtelConfig;
|
||||
use codex_config::types::OtelConfigToml;
|
||||
use codex_config::types::OtelExporterKind;
|
||||
use codex_config::types::ShellEnvironmentPolicy;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
use codex_config::types::TuiNotificationSettings;
|
||||
@@ -74,6 +73,7 @@ use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchConfig;
|
||||
@@ -115,6 +115,7 @@ pub use codex_config::Constrained;
|
||||
pub use codex_config::ConstraintError;
|
||||
pub use codex_config::ConstraintResult;
|
||||
pub use codex_network_proxy::NetworkProxyAuditMetadata;
|
||||
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
|
||||
pub use codex_sandboxing::system_bwrap_warning;
|
||||
pub use managed_features::ManagedFeatures;
|
||||
pub use network_proxy_spec::NetworkProxySpec;
|
||||
@@ -191,14 +192,9 @@ pub(crate) async fn test_config() -> Config {
|
||||
pub struct Permissions {
|
||||
/// Approval policy for executing commands.
|
||||
pub approval_policy: Constrained<AskForApproval>,
|
||||
/// Effective sandbox policy used for shell/unified exec.
|
||||
pub sandbox_policy: Constrained<SandboxPolicy>,
|
||||
/// Effective filesystem sandbox policy, including entries that cannot yet
|
||||
/// be fully represented by the legacy [`SandboxPolicy`] projection.
|
||||
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
/// Effective network sandbox policy split out from the legacy
|
||||
/// [`SandboxPolicy`] projection.
|
||||
pub network_sandbox_policy: NetworkSandboxPolicy,
|
||||
/// Canonical effective runtime permissions after config requirements and
|
||||
/// runtime readable-root additions have been applied.
|
||||
pub permission_profile: Constrained<PermissionProfile>,
|
||||
/// Effective network configuration applied to all spawned processes.
|
||||
pub network: Option<NetworkProxySpec>,
|
||||
/// Whether the model may request a login shell for shell-based tools.
|
||||
@@ -223,12 +219,99 @@ impl Permissions {
|
||||
/// Effective runtime permissions after config requirements and runtime
|
||||
/// readable-root additions have been applied.
|
||||
pub fn permission_profile(&self) -> PermissionProfile {
|
||||
PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()),
|
||||
&self.file_system_sandbox_policy,
|
||||
self.network_sandbox_policy,
|
||||
self.permission_profile.get().clone()
|
||||
}
|
||||
|
||||
/// Effective filesystem sandbox policy derived from the canonical profile.
|
||||
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
|
||||
self.permission_profile.get().file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
/// Effective network sandbox policy derived from the canonical profile.
|
||||
pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
|
||||
self.permission_profile.get().network_sandbox_policy()
|
||||
}
|
||||
|
||||
/// Legacy compatibility projection derived from the canonical profile.
|
||||
pub fn legacy_sandbox_policy(&self, cwd: &Path) -> SandboxPolicy {
|
||||
let permission_profile = self.permission_profile.get();
|
||||
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
|
||||
compatibility_sandbox_policy_for_permission_profile(
|
||||
permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
permission_profile.network_sandbox_policy(),
|
||||
cwd,
|
||||
)
|
||||
}
|
||||
|
||||
/// Check whether a legacy sandbox policy can be applied to this permission
|
||||
/// set after projecting it into the canonical permission profile.
|
||||
pub fn can_set_legacy_sandbox_policy(
|
||||
&self,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> ConstraintResult<()> {
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
self.permission_profile.can_set(&permission_profile)
|
||||
}
|
||||
|
||||
/// Replace permissions from a legacy sandbox policy and keep every
|
||||
/// permission projection in sync.
|
||||
pub fn set_legacy_sandbox_policy(
|
||||
&mut self,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> ConstraintResult<()> {
|
||||
self.can_set_legacy_sandbox_policy(&sandbox_policy, cwd)?;
|
||||
let file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
|
||||
self.permission_profile.set(permission_profile)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replace permissions from the canonical profile.
|
||||
pub fn set_permission_profile(
|
||||
&mut self,
|
||||
permission_profile: PermissionProfile,
|
||||
) -> ConstraintResult<()> {
|
||||
self.permission_profile.can_set(&permission_profile)?;
|
||||
|
||||
self.permission_profile.set(permission_profile)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn constrained_permission_profile_from_sandbox_projection(
|
||||
initial_value: PermissionProfile,
|
||||
sandbox_constraint: Constrained<SandboxPolicy>,
|
||||
cwd: AbsolutePathBuf,
|
||||
) -> std::io::Result<Constrained<PermissionProfile>> {
|
||||
Constrained::new(initial_value, move |candidate| {
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
candidate.to_runtime_permissions();
|
||||
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
candidate,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
cwd.as_path(),
|
||||
);
|
||||
sandbox_constraint.can_set(&sandbox_policy)
|
||||
})
|
||||
.map_err(std::io::Error::from)
|
||||
}
|
||||
|
||||
/// Configured thread persistence backend.
|
||||
@@ -687,7 +770,7 @@ impl AuthManagerConfig for Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ConfigBuilder {
|
||||
codex_home: Option<PathBuf>,
|
||||
cli_overrides: Option<Vec<(String, TomlValue)>>,
|
||||
@@ -696,22 +779,6 @@ pub struct ConfigBuilder {
|
||||
cloud_requirements: CloudRequirementsLoader,
|
||||
thread_config_loader: Option<Arc<dyn ThreadConfigLoader>>,
|
||||
fallback_cwd: Option<PathBuf>,
|
||||
host_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ConfigBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
codex_home: None,
|
||||
cli_overrides: None,
|
||||
harness_overrides: None,
|
||||
loader_overrides: None,
|
||||
cloud_requirements: CloudRequirementsLoader::default(),
|
||||
thread_config_loader: None,
|
||||
fallback_cwd: None,
|
||||
host_name: codex_config::host_name(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConfigBuilder {
|
||||
@@ -753,11 +820,6 @@ impl ConfigBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn host_name(mut self, host_name: Option<String>) -> Self {
|
||||
self.host_name = host_name;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build(self) -> std::io::Result<Config> {
|
||||
let Self {
|
||||
codex_home,
|
||||
@@ -767,7 +829,6 @@ impl ConfigBuilder {
|
||||
cloud_requirements,
|
||||
thread_config_loader,
|
||||
fallback_cwd,
|
||||
host_name,
|
||||
} = self;
|
||||
let codex_home = match codex_home {
|
||||
Some(codex_home) => AbsolutePathBuf::from_absolute_path(codex_home)?,
|
||||
@@ -792,7 +853,6 @@ impl ConfigBuilder {
|
||||
thread_config_loader
|
||||
.as_deref()
|
||||
.unwrap_or(&codex_config::NoopThreadConfigLoader),
|
||||
host_name.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
@@ -804,10 +864,13 @@ impl ConfigBuilder {
|
||||
let config_toml: ConfigToml = match merged_toml.try_into() {
|
||||
Ok(config_toml) => config_toml,
|
||||
Err(err) => {
|
||||
if let Some(config_error) =
|
||||
crate::config_loader::first_layer_config_error(&config_layer_stack).await
|
||||
if let Some(config_error) = codex_config::first_layer_config_error::<ConfigToml>(
|
||||
&config_layer_stack,
|
||||
codex_config::CONFIG_TOML_FILE,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(crate::config_loader::io_error_from_config_error(
|
||||
return Err(codex_config::io_error_from_config_error(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
config_error,
|
||||
Some(err),
|
||||
@@ -833,6 +896,18 @@ impl ConfigBuilder {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn legacy_sandbox_policy(&self) -> SandboxPolicy {
|
||||
self.permissions.legacy_sandbox_policy(self.cwd.as_path())
|
||||
}
|
||||
|
||||
pub fn set_legacy_sandbox_policy(
|
||||
&mut self,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
) -> ConstraintResult<()> {
|
||||
self.permissions
|
||||
.set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path())
|
||||
}
|
||||
|
||||
pub fn to_models_manager_config(&self) -> ModelsManagerConfig {
|
||||
ModelsManagerConfig {
|
||||
model_context_window: self.model_context_window,
|
||||
@@ -907,8 +982,8 @@ impl Config {
|
||||
format!("failed to serialize default config: {e}"),
|
||||
)
|
||||
})?;
|
||||
let cli_layer = crate::config_loader::build_cli_overrides_layer(&cli_overrides);
|
||||
crate::config_loader::merge_toml_values(&mut merged, &cli_layer);
|
||||
let cli_layer = codex_config::build_cli_overrides_layer(&cli_overrides);
|
||||
codex_config::merge_toml_values(&mut merged, &cli_layer);
|
||||
let codex_home = AbsolutePathBuf::from_absolute_path_checked(codex_home)?;
|
||||
let config_toml = deserialize_config_toml_with_base(merged, &codex_home)?;
|
||||
Self::load_config_with_layer_stack(
|
||||
@@ -972,7 +1047,6 @@ pub async fn load_config_as_toml_with_cli_and_loader_overrides(
|
||||
loader_overrides,
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1154,7 +1228,6 @@ pub async fn load_global_mcp_servers(
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
&codex_config::NoopThreadConfigLoader,
|
||||
/*host_name*/ None,
|
||||
)
|
||||
.await?;
|
||||
let merged_toml = config_layer_stack.effective_config();
|
||||
@@ -1390,7 +1463,7 @@ fn resolve_permission_config_syntax(
|
||||
|
||||
fn apply_managed_filesystem_constraints(
|
||||
file_system_sandbox_policy: &mut FileSystemSandboxPolicy,
|
||||
filesystem_constraints: &crate::config_loader::FilesystemConstraints,
|
||||
filesystem_constraints: &codex_config::FilesystemConstraints,
|
||||
) {
|
||||
for deny_read in &filesystem_constraints.deny_read {
|
||||
let deny_entry = if deny_read.contains_glob() {
|
||||
@@ -1567,11 +1640,11 @@ fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiA
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> WebSearchMode {
|
||||
let preferred = web_search_mode.value();
|
||||
|
||||
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
|
||||
if matches!(permission_profile, PermissionProfile::Disabled)
|
||||
&& preferred != WebSearchMode::Disabled
|
||||
{
|
||||
for mode in [
|
||||
@@ -1807,10 +1880,11 @@ impl Config {
|
||||
&& has_permission_profiles);
|
||||
let (
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
) = if let Some(permission_profile) = permission_profile {
|
||||
) = if let Some(mut permission_profile) = permission_profile {
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let configured_network_proxy_config =
|
||||
@@ -1836,25 +1910,33 @@ impl Config {
|
||||
} else {
|
||||
NetworkProxyConfig::default()
|
||||
};
|
||||
let mut sandbox_policy = permission_profile
|
||||
.to_legacy_sandbox_policy(resolved_cwd.as_path())
|
||||
.map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("invalid permission_profile override: {err}"),
|
||||
)
|
||||
})?;
|
||||
let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&additional_writable_roots,
|
||||
);
|
||||
sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
permission_profile.enforcement(),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
}
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -1882,19 +1964,36 @@ impl Config {
|
||||
resolved_cwd.as_path(),
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let mut sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
let mut permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
file_system_sandbox_policy = file_system_sandbox_policy
|
||||
.with_additional_writable_roots(
|
||||
resolved_cwd.as_path(),
|
||||
&additional_writable_roots,
|
||||
);
|
||||
sandbox_policy = file_system_sandbox_policy
|
||||
.to_legacy_sandbox_policy(network_sandbox_policy, resolved_cwd.as_path())?;
|
||||
permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
}
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -1923,8 +2022,14 @@ impl Config {
|
||||
resolved_cwd.as_path(),
|
||||
);
|
||||
let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
);
|
||||
(
|
||||
configured_network_proxy_config,
|
||||
permission_profile,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
@@ -2010,13 +2115,6 @@ impl Config {
|
||||
|
||||
let history = cfg.history.unwrap_or_default();
|
||||
|
||||
let agent_max_threads_from_config = cfg.agents.as_ref().and_then(|agents| agents.max_threads);
|
||||
if features.enabled(Feature::MultiAgentV2) && agent_max_threads_from_config.is_some() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"agents.max_threads cannot be set when multi_agent_v2 is enabled",
|
||||
));
|
||||
}
|
||||
let agent_max_threads = cfg
|
||||
.agents
|
||||
.as_ref()
|
||||
@@ -2289,10 +2387,17 @@ impl Config {
|
||||
None => (None, None),
|
||||
};
|
||||
let has_network_requirements = network_requirements.is_some();
|
||||
let network_permission_profile = if *constrained_sandbox_policy.get()
|
||||
== original_sandbox_policy
|
||||
{
|
||||
permission_profile.clone()
|
||||
} else {
|
||||
PermissionProfile::from_legacy_sandbox_policy(constrained_sandbox_policy.get())
|
||||
};
|
||||
let network = NetworkProxySpec::from_config_and_constraints(
|
||||
configured_network_proxy_config,
|
||||
network_requirements,
|
||||
constrained_sandbox_policy.get(),
|
||||
&network_permission_profile,
|
||||
)
|
||||
.map_err(|err| {
|
||||
if let Some(source) = network_requirements_source.as_ref() {
|
||||
@@ -2343,6 +2448,22 @@ impl Config {
|
||||
} else {
|
||||
NetworkSandboxPolicy::from(&effective_sandbox_policy)
|
||||
};
|
||||
let effective_enforcement = if effective_sandbox_policy == original_sandbox_policy {
|
||||
permission_profile.enforcement()
|
||||
} else {
|
||||
SandboxEnforcement::from_legacy_sandbox_policy(&effective_sandbox_policy)
|
||||
};
|
||||
let effective_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
|
||||
effective_enforcement,
|
||||
&effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
);
|
||||
let constrained_permission_profile =
|
||||
constrained_permission_profile_from_sandbox_projection(
|
||||
effective_permission_profile,
|
||||
constrained_sandbox_policy.value.clone(),
|
||||
resolved_cwd.clone(),
|
||||
)?;
|
||||
let config = Self {
|
||||
model,
|
||||
service_tier,
|
||||
@@ -2355,9 +2476,7 @@ impl Config {
|
||||
startup_warnings,
|
||||
permissions: Permissions {
|
||||
approval_policy: constrained_approval_policy.value,
|
||||
sandbox_policy: constrained_sandbox_policy.value,
|
||||
file_system_sandbox_policy: effective_file_system_sandbox_policy,
|
||||
network_sandbox_policy: effective_network_sandbox_policy,
|
||||
permission_profile: constrained_permission_profile,
|
||||
network,
|
||||
allow_login_shell,
|
||||
shell_environment_policy,
|
||||
@@ -2610,8 +2729,8 @@ impl Config {
|
||||
|
||||
pub fn managed_network_requirements_enabled(&self) -> bool {
|
||||
!matches!(
|
||||
self.permissions.sandbox_policy.get(),
|
||||
SandboxPolicy::DangerFullAccess
|
||||
self.permissions.permission_profile.get(),
|
||||
PermissionProfile::Disabled
|
||||
) && self
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
@@ -2682,3 +2801,7 @@ pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
|
||||
#[cfg(test)]
|
||||
#[path = "config_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "config_loader_tests.rs"]
|
||||
mod config_loader_tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::config_loader::NetworkConstraints;
|
||||
use async_trait::async_trait;
|
||||
use codex_config::NetworkConstraints;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_network_proxy::BlockedRequestObserver;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
@@ -16,7 +16,7 @@ use codex_network_proxy::build_config_state;
|
||||
use codex_network_proxy::host_and_port_from_network_addr;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -89,7 +89,7 @@ impl NetworkProxySpec {
|
||||
pub(crate) fn from_config_and_constraints(
|
||||
config: NetworkProxyConfig,
|
||||
requirements: Option<NetworkConstraints>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> std::io::Result<Self> {
|
||||
let base_config = config.clone();
|
||||
let hard_deny_allowlist_misses = requirements
|
||||
@@ -99,7 +99,7 @@ impl NetworkProxySpec {
|
||||
Self::apply_requirements(
|
||||
config,
|
||||
requirements,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
hard_deny_allowlist_misses,
|
||||
)
|
||||
} else {
|
||||
@@ -122,7 +122,7 @@ impl NetworkProxySpec {
|
||||
|
||||
pub async fn start_proxy(
|
||||
&self,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,
|
||||
enable_network_approval_flow: bool,
|
||||
@@ -133,10 +133,7 @@ impl NetworkProxySpec {
|
||||
if enable_network_approval_flow && !self.hard_deny_allowlist_misses {
|
||||
if let Some(policy_decider) = policy_decider {
|
||||
builder = builder.policy_decider_arc(policy_decider);
|
||||
} else if matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
|
||||
) {
|
||||
} else if Self::managed_sandbox_active(permission_profile) {
|
||||
builder = builder
|
||||
.policy_decider(|_request| async { NetworkDecision::ask("not_allowed") });
|
||||
}
|
||||
@@ -154,14 +151,14 @@ impl NetworkProxySpec {
|
||||
Ok(StartedNetworkProxy::new(proxy, handle))
|
||||
}
|
||||
|
||||
pub(crate) fn recompute_for_sandbox_policy(
|
||||
pub(crate) fn recompute_for_permission_profile(
|
||||
&self,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
) -> std::io::Result<Self> {
|
||||
Self::from_config_and_constraints(
|
||||
self.base_config.clone(),
|
||||
self.requirements.clone(),
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -216,13 +213,13 @@ impl NetworkProxySpec {
|
||||
fn apply_requirements(
|
||||
mut config: NetworkProxyConfig,
|
||||
requirements: &NetworkConstraints,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
hard_deny_allowlist_misses: bool,
|
||||
) -> (NetworkProxyConfig, NetworkProxyConstraints) {
|
||||
let mut constraints = NetworkProxyConstraints::default();
|
||||
let allowlist_expansion_enabled =
|
||||
Self::allowlist_expansion_enabled(sandbox_policy, hard_deny_allowlist_misses);
|
||||
let denylist_expansion_enabled = Self::denylist_expansion_enabled(sandbox_policy);
|
||||
Self::allowlist_expansion_enabled(permission_profile, hard_deny_allowlist_misses);
|
||||
let denylist_expansion_enabled = Self::denylist_expansion_enabled(permission_profile);
|
||||
|
||||
if let Some(enabled) = requirements.enabled {
|
||||
config.network.enabled = enabled;
|
||||
@@ -322,24 +319,22 @@ impl NetworkProxySpec {
|
||||
}
|
||||
|
||||
fn allowlist_expansion_enabled(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
hard_deny_allowlist_misses: bool,
|
||||
) -> bool {
|
||||
matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
|
||||
) && !hard_deny_allowlist_misses
|
||||
Self::managed_sandbox_active(permission_profile) && !hard_deny_allowlist_misses
|
||||
}
|
||||
|
||||
fn managed_allowed_domains_only(requirements: &NetworkConstraints) -> bool {
|
||||
requirements.managed_allowed_domains_only.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn denylist_expansion_enabled(sandbox_policy: &SandboxPolicy) -> bool {
|
||||
matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. }
|
||||
)
|
||||
fn denylist_expansion_enabled(permission_profile: &PermissionProfile) -> bool {
|
||||
Self::managed_sandbox_active(permission_profile)
|
||||
}
|
||||
|
||||
fn managed_sandbox_active(permission_profile: &PermissionProfile) -> bool {
|
||||
matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
}
|
||||
|
||||
fn merge_domain_lists(mut managed: Vec<String>, user_entries: &[String]) -> Vec<String> {
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use super::*;
|
||||
use crate::config_loader::NetworkDomainPermissionToml;
|
||||
use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use codex_config::NetworkDomainPermissionToml;
|
||||
use codex_config::NetworkDomainPermissionsToml;
|
||||
use codex_network_proxy::NetworkDomainPermission;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
|
||||
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
|
||||
}
|
||||
|
||||
fn domain_permissions(
|
||||
entries: impl IntoIterator<Item = (&'static str, NetworkDomainPermissionToml)>,
|
||||
) -> NetworkDomainPermissionsToml {
|
||||
@@ -54,7 +62,7 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_read_only_policy()),
|
||||
)
|
||||
.expect("config should stay within the managed allowlist");
|
||||
|
||||
@@ -89,7 +97,7 @@ fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("managed allowlist should not erase a user deny");
|
||||
|
||||
@@ -121,7 +129,7 @@ fn requirements_allowlist_expansion_keeps_user_entries_mutable() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("managed baseline should still allow user edits");
|
||||
|
||||
@@ -144,6 +152,41 @@ fn requirements_allowlist_expansion_keeps_user_entries_mutable() {
|
||||
.expect("user allowlist entries should not become managed constraints");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_unrestricted_profile_allows_domain_expansion() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
config
|
||||
.network
|
||||
.set_allowed_domains(vec!["api.example.com".to_string()]);
|
||||
let requirements = NetworkConstraints {
|
||||
domains: Some(domain_permissions([(
|
||||
"*.example.com",
|
||||
NetworkDomainPermissionToml::Allow,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let permission_profile = PermissionProfile::Managed {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
};
|
||||
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&permission_profile,
|
||||
)
|
||||
.expect("managed unrestricted filesystem should still use managed network constraints");
|
||||
|
||||
assert_eq!(
|
||||
spec.config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"*.example.com".to_string(),
|
||||
"api.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(spec.constraints.allowlist_expansion_enabled, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
@@ -164,7 +207,7 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
|
||||
)
|
||||
.expect("yolo mode should pin the effective policy to the managed baseline");
|
||||
|
||||
@@ -198,7 +241,7 @@ fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("managed baseline should still load");
|
||||
|
||||
@@ -227,7 +270,7 @@ fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses()
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("managed-only allowlist should still load");
|
||||
|
||||
@@ -257,7 +300,7 @@ fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domain
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("managed-only mode should treat missing managed allowlist as empty");
|
||||
|
||||
@@ -281,7 +324,7 @@ fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_m
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
|
||||
)
|
||||
.expect("managed-only mode should treat missing managed allowlist as empty");
|
||||
|
||||
@@ -308,7 +351,7 @@ fn deny_only_requirements_do_not_create_allow_constraints_in_full_access() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
|
||||
)
|
||||
.expect("deny-only requirements should not constrain the allowlist");
|
||||
|
||||
@@ -341,7 +384,7 @@ fn allow_only_requirements_do_not_create_deny_constraints_in_full_access() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
|
||||
)
|
||||
.expect("allow-only requirements should not constrain the denylist");
|
||||
|
||||
@@ -374,7 +417,7 @@ fn requirements_denied_domains_are_a_baseline_for_default_mode() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("default mode should merge managed and user deny entries");
|
||||
|
||||
@@ -409,7 +452,7 @@ fn requirements_denylist_expansion_keeps_user_entries_mutable() {
|
||||
let spec = NetworkProxySpec::from_config_and_constraints(
|
||||
config,
|
||||
Some(requirements),
|
||||
&SandboxPolicy::new_workspace_write_policy(),
|
||||
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
|
||||
)
|
||||
.expect("managed baseline should still allow user edits");
|
||||
|
||||
|
||||
@@ -383,6 +383,16 @@ fn validate_glob_scan_max_depth(max_depth: Option<usize>) -> io::Result<Option<u
|
||||
}
|
||||
|
||||
fn contains_glob_chars(path: &str) -> bool {
|
||||
contains_glob_chars_for_platform(path, cfg!(windows))
|
||||
}
|
||||
|
||||
fn contains_glob_chars_for_platform(path: &str, is_windows: bool) -> bool {
|
||||
let normalized_windows_path = if is_windows {
|
||||
normalize_windows_device_path(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let path = normalized_windows_path.as_deref().unwrap_or(path);
|
||||
path.chars().any(|ch| matches!(ch, '*' | '?' | '[' | ']'))
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,18 @@ fn normalize_absolute_path_for_platform_simplifies_windows_verbatim_paths() {
|
||||
assert_eq!(parsed, PathBuf::from(r"D:\c\x\worktrees\2508\swift-base"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_verbatim_path_prefix_does_not_count_as_glob_syntax() {
|
||||
assert!(!contains_glob_chars_for_platform(
|
||||
r"\\?\D:\c\x\worktrees\2508\swift-base",
|
||||
/*is_windows*/ true,
|
||||
));
|
||||
assert!(contains_glob_chars_for_platform(
|
||||
r"\\?\D:\c\x\worktrees\2508\**\*.env",
|
||||
/*is_windows*/ true,
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
@@ -77,7 +89,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
|
||||
let expected_zsh = AbsolutePathBuf::try_from(zsh_path)?;
|
||||
let expected_allowed_arg0_dir = AbsolutePathBuf::try_from(allowed_arg0_dir)?;
|
||||
let expected_sibling_arg0_dir = AbsolutePathBuf::try_from(sibling_arg0_dir)?;
|
||||
let policy = &config.permissions.file_system_sandbox_policy;
|
||||
let policy = config.permissions.file_system_sandbox_policy();
|
||||
|
||||
assert!(
|
||||
policy.can_read_path_with_cwd(expected_zsh.as_path(), &cwd),
|
||||
|
||||
@@ -17,7 +17,7 @@ use codex_connectors::DirectoryListResponse;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::EnvironmentManagerArgs;
|
||||
use codex_exec_server::ExecServerRuntimePaths;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde::Deserialize;
|
||||
@@ -25,11 +25,11 @@ use serde::de::DeserializeOwned;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::config_loader::AppsRequirementsToml;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::plugins::list_tool_suggest_discoverable_plugins;
|
||||
use crate::session::INITIAL_SUBMIT_ID;
|
||||
use codex_config::AppsRequirementsToml;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::AppsConfigToml;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
@@ -274,7 +274,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
|
||||
&config.permissions.approval_policy,
|
||||
INITIAL_SUBMIT_ID.to_owned(),
|
||||
tx_event,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
PermissionProfile::default(),
|
||||
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()),
|
||||
config.codex_home.to_path_buf(),
|
||||
codex_apps_tools_cache_key(auth.as_ref()),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use super::*;
|
||||
use crate::config::CONFIG_TOML_FILE;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::AppRequirementToml;
|
||||
use crate::config_loader::AppsRequirementsToml;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use codex_config::AppRequirementToml;
|
||||
use codex_config::AppsRequirementsToml;
|
||||
use codex_config::CloudRequirementsLoader;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::types::AppConfig;
|
||||
use codex_config::types::AppToolConfig;
|
||||
use codex_config::types::AppToolsConfig;
|
||||
|
||||
@@ -81,7 +81,6 @@ pub trait ContextualUserFragment {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: self.render(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ use super::ContextualUserFragment;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::format_allow_prefixes;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::NetworkAccess;
|
||||
@@ -57,7 +59,33 @@ pub struct PermissionsInstructions {
|
||||
}
|
||||
|
||||
impl PermissionsInstructions {
|
||||
/// Builds permissions instructions from the effective sandbox and approval policy.
|
||||
/// Builds permissions instructions from the effective permission profile and approval policy.
|
||||
pub fn from_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
exec_policy: &Policy,
|
||||
cwd: &Path,
|
||||
exec_permission_approvals_enabled: bool,
|
||||
request_permissions_tool_enabled: bool,
|
||||
) -> Self {
|
||||
let (sandbox_mode, writable_roots) = sandbox_prompt_from_profile(permission_profile, cwd);
|
||||
|
||||
Self::from_permissions_with_network(
|
||||
sandbox_mode,
|
||||
network_access_from_policy(permission_profile.network_sandbox_policy()),
|
||||
PermissionsPromptConfig {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
exec_policy,
|
||||
exec_permission_approvals_enabled,
|
||||
request_permissions_tool_enabled,
|
||||
},
|
||||
writable_roots,
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds permissions instructions from a legacy sandbox policy.
|
||||
pub fn from_policy(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
approval_policy: AskForApproval,
|
||||
@@ -67,33 +95,14 @@ impl PermissionsInstructions {
|
||||
exec_permission_approvals_enabled: bool,
|
||||
request_permissions_tool_enabled: bool,
|
||||
) -> Self {
|
||||
let network_access = if sandbox_policy.has_full_network_access() {
|
||||
NetworkAccess::Enabled
|
||||
} else {
|
||||
NetworkAccess::Restricted
|
||||
};
|
||||
|
||||
let (sandbox_mode, writable_roots) = match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None),
|
||||
SandboxPolicy::ReadOnly { .. } => (SandboxMode::ReadOnly, None),
|
||||
SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let roots = sandbox_policy.get_writable_roots_with_cwd(cwd);
|
||||
(SandboxMode::WorkspaceWrite, Some(roots))
|
||||
}
|
||||
};
|
||||
|
||||
Self::from_permissions_with_network(
|
||||
sandbox_mode,
|
||||
network_access,
|
||||
PermissionsPromptConfig {
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
exec_policy,
|
||||
exec_permission_approvals_enabled,
|
||||
request_permissions_tool_enabled,
|
||||
},
|
||||
writable_roots,
|
||||
Self::from_permission_profile(
|
||||
&PermissionProfile::from_legacy_sandbox_policy(sandbox_policy),
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
exec_policy,
|
||||
cwd,
|
||||
exec_permission_approvals_enabled,
|
||||
request_permissions_tool_enabled,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,6 +134,38 @@ impl PermissionsInstructions {
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox_prompt_from_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
) -> (SandboxMode, Option<Vec<WritableRoot>>) {
|
||||
match permission_profile {
|
||||
PermissionProfile::Disabled | PermissionProfile::External { .. } => {
|
||||
(SandboxMode::DangerFullAccess, None)
|
||||
}
|
||||
PermissionProfile::Managed { .. } => {
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
if file_system_policy.has_full_disk_write_access() {
|
||||
return (SandboxMode::DangerFullAccess, None);
|
||||
}
|
||||
|
||||
let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd);
|
||||
if writable_roots.is_empty() {
|
||||
(SandboxMode::ReadOnly, None)
|
||||
} else {
|
||||
(SandboxMode::WorkspaceWrite, Some(writable_roots))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn network_access_from_policy(network_policy: NetworkSandboxPolicy) -> NetworkAccess {
|
||||
if network_policy.is_enabled() {
|
||||
NetworkAccess::Enabled
|
||||
} else {
|
||||
NetworkAccess::Restricted
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for PermissionsInstructions {
|
||||
const ROLE: &'static str = "developer";
|
||||
const START_MARKER: &'static str = "<permissions instructions>";
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use super::*;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -70,6 +76,36 @@ fn builds_permissions_from_policy() {
|
||||
assert!(text.contains("`approval_policy` is `unless-trusted`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_permissions_from_profile() {
|
||||
let cwd = PathBuf::from("/tmp");
|
||||
let writable_root =
|
||||
AbsolutePathBuf::from_absolute_path(cwd.join("repo")).expect("absolute path");
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: writable_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
);
|
||||
|
||||
let instructions = PermissionsInstructions::from_permission_profile(
|
||||
&permission_profile,
|
||||
AskForApproval::UnlessTrusted,
|
||||
ApprovalsReviewer::User,
|
||||
&Policy::empty(),
|
||||
&cwd,
|
||||
/*exec_permission_approvals_enabled*/ false,
|
||||
/*request_permissions_tool_enabled*/ false,
|
||||
);
|
||||
let text = instructions.body();
|
||||
assert!(text.contains("`sandbox_mode` is `workspace-write`"));
|
||||
assert!(text.contains("Network access is enabled."));
|
||||
assert!(text.contains(writable_root.to_string_lossy().as_ref()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn includes_request_rule_instructions_for_on_request() {
|
||||
let mut exec_policy = Policy::empty();
|
||||
|
||||
@@ -41,7 +41,6 @@ fn assistant_msg(text: &str) -> ResponseItem {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
@@ -60,7 +59,6 @@ fn inter_agent_assistant_msg(text: &str) -> ResponseItem {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: serde_json::to_string(&communication).unwrap(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
@@ -80,7 +78,6 @@ fn user_msg(text: &str) -> ResponseItem {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
@@ -92,7 +89,6 @@ fn user_input_text_msg(text: &str) -> ResponseItem {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
@@ -104,7 +100,6 @@ fn developer_msg(text: &str) -> ResponseItem {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: text.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
@@ -119,7 +114,6 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem {
|
||||
text: (*text).to_string(),
|
||||
})
|
||||
.collect(),
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
@@ -200,7 +194,6 @@ fn filters_non_api_messages() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "ignored".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
let reasoning = reasoning_msg("thinking...");
|
||||
@@ -231,7 +224,6 @@ fn filters_non_api_messages() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "hi".to_string()
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -240,7 +232,6 @@ fn filters_non_api_messages() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "hello".to_string()
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
]
|
||||
@@ -390,7 +381,6 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
|
||||
text: "caption".to_string(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::FunctionCall {
|
||||
@@ -453,7 +443,6 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
|
||||
text: "caption".to_string(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::FunctionCall {
|
||||
@@ -512,7 +501,6 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}]);
|
||||
let preserved = with_images.for_prompt(&modalities);
|
||||
@@ -540,7 +528,6 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "hi".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
]);
|
||||
@@ -560,7 +547,6 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "hi".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
]
|
||||
@@ -576,7 +562,6 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "generate a lobster".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::ImageGenerationCall {
|
||||
@@ -596,7 +581,6 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "generate a lobster".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::ImageGenerationCall {
|
||||
@@ -758,7 +742,6 @@ fn replace_last_turn_images_does_not_touch_user_images() {
|
||||
image_url: "data:image/png;base64,AAA".to_string(),
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}];
|
||||
let mut history = create_history_with_items(items.clone());
|
||||
@@ -1690,7 +1673,6 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
let text_only_item = ResponseItem::Message {
|
||||
@@ -1699,7 +1681,6 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Here is the screenshot".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -1773,7 +1754,6 @@ fn non_base64_image_urls_are_unchanged() {
|
||||
image_url: "https://example.com/foo.png".to_string(),
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
let function_output_item = ResponseItem::FunctionCallOutput {
|
||||
@@ -1805,7 +1785,6 @@ fn data_url_without_base64_marker_is_unchanged() {
|
||||
image_url: "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'/>".to_string(),
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -1846,7 +1825,6 @@ fn mixed_case_data_url_markers_are_adjusted() {
|
||||
image_url,
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -1879,7 +1857,6 @@ fn multiple_inline_images_apply_multiple_fixed_costs() {
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -1962,7 +1939,6 @@ fn text_only_items_unchanged() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "Hello world, this is a response.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ fn build_permissions_update_item(
|
||||
}
|
||||
|
||||
Some(
|
||||
PermissionsInstructions::from_policy(
|
||||
next.sandbox_policy.get(),
|
||||
PermissionsInstructions::from_permission_profile(
|
||||
&next.permission_profile,
|
||||
next.approval_policy.value(),
|
||||
next.config.approvals_reviewer,
|
||||
exec_policy,
|
||||
@@ -197,7 +197,6 @@ fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<Response
|
||||
id: None,
|
||||
role: role.to_string(),
|
||||
content,
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ fn parses_user_message_with_text_and_two_images() {
|
||||
detail: Some(DEFAULT_IMAGE_DETAIL),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -78,7 +77,6 @@ fn skips_local_image_label_text() {
|
||||
text: user_text.clone(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -108,7 +106,6 @@ fn parses_assistant_message_input_text_for_backward_compatibility() {
|
||||
text: "author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue"
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -158,7 +155,6 @@ fn skips_unnamed_image_label_text() {
|
||||
text: user_text.clone(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -188,7 +184,6 @@ fn skips_user_instructions_and_env() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -197,7 +192,6 @@ fn skips_user_instructions_and_env() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<environment_context>test_text</environment_context>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -206,7 +200,6 @@ fn skips_user_instructions_and_env() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -216,7 +209,6 @@ fn skips_user_instructions_and_env() {
|
||||
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -225,7 +217,6 @@ fn skips_user_instructions_and_env() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -241,7 +232,6 @@ fn skips_user_instructions_and_env() {
|
||||
.to_string(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -292,7 +282,6 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() {
|
||||
.to_string(),
|
||||
},
|
||||
],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
@@ -321,7 +310,6 @@ fn parses_agent_message() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "Hello from Codex".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ use codex_protocol::error::Result;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
@@ -220,9 +221,7 @@ pub struct StdoutStream {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn process_exec_tool_call(
|
||||
params: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_legacy_landlock: bool,
|
||||
@@ -230,9 +229,7 @@ pub async fn process_exec_tool_call(
|
||||
) -> Result<ExecToolCallOutput> {
|
||||
let exec_req = build_exec_request(
|
||||
params,
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
permission_profile,
|
||||
sandbox_cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
@@ -246,9 +243,7 @@ pub async fn process_exec_tool_call(
|
||||
/// spawned under the requested sandbox policy.
|
||||
pub fn build_exec_request(
|
||||
params: ExecParams,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_cwd: &AbsolutePathBuf,
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_legacy_landlock: bool,
|
||||
@@ -271,8 +266,10 @@ pub fn build_exec_request(
|
||||
} = params;
|
||||
|
||||
let enforce_managed_network = network.is_some();
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
let sandbox_type = select_process_exec_tool_sandbox_type(
|
||||
file_system_sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
windows_sandbox_level,
|
||||
enforce_managed_network,
|
||||
@@ -304,9 +301,7 @@ pub fn build_exec_request(
|
||||
let mut exec_req = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command,
|
||||
policy: sandbox_policy,
|
||||
file_system_policy: file_system_sandbox_policy,
|
||||
network_policy: network_sandbox_policy,
|
||||
permissions: permission_profile,
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
@@ -366,6 +361,7 @@ pub(crate) async fn execute_exec_request(
|
||||
windows_sandbox_policy_cwd: _,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
permission_profile: _,
|
||||
sandbox_policy,
|
||||
// TODO(mbolin): Use file_system_sandbox_policy instead of sandbox_policy.
|
||||
file_system_sandbox_policy: _,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#[cfg(test)]
|
||||
use codex_config::types::EnvironmentVariablePattern;
|
||||
use codex_config::types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::ThreadId;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::config_types::EnvironmentVariablePattern;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::shell_environment;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use codex_config::shell_environment::CODEX_THREAD_ID_ENV_VAR;
|
||||
pub use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR;
|
||||
|
||||
/// Construct an environment map based on the rules in the specified policy. The
|
||||
/// resulting map can be passed directly to `Command::envs()` after calling
|
||||
@@ -21,7 +22,7 @@ pub fn create_env(
|
||||
thread_id: Option<ThreadId>,
|
||||
) -> HashMap<String, String> {
|
||||
let thread_id = thread_id.map(|thread_id| thread_id.to_string());
|
||||
codex_config::shell_environment::create_env(policy, thread_id.as_deref())
|
||||
shell_environment::create_env(policy, thread_id.as_deref())
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
@@ -34,7 +35,7 @@ where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
let thread_id = thread_id.map(|thread_id| thread_id.to_string());
|
||||
codex_config::shell_environment::create_env_from_vars(vars, policy, thread_id.as_deref())
|
||||
shell_environment::create_env_from_vars(vars, policy, thread_id.as_deref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -47,7 +48,7 @@ where
|
||||
I: IntoIterator<Item = (String, String)>,
|
||||
{
|
||||
let thread_id = thread_id.map(|thread_id| thread_id.to_string());
|
||||
codex_config::shell_environment::populate_env(vars, policy, thread_id.as_deref())
|
||||
shell_environment::populate_env(vars, policy, thread_id.as_deref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use codex_config::types::ShellEnvironmentPolicyInherit;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use maplit::hashmap;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_execpolicy::AmendError;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Error as ExecPolicyRuleError;
|
||||
@@ -20,10 +20,10 @@ use codex_execpolicy::RuleMatch;
|
||||
use codex_execpolicy::blocking_append_allow_prefix_rule;
|
||||
use codex_execpolicy::blocking_append_network_rule;
|
||||
use codex_protocol::approvals::ExecPolicyAmendment;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_shell_command::is_dangerous_command::command_might_be_dangerous;
|
||||
use codex_shell_command::is_safe_command::is_known_safe_command;
|
||||
use thiserror::Error;
|
||||
@@ -204,8 +204,9 @@ pub(crate) struct ExecPolicyManager {
|
||||
pub(crate) struct ExecApprovalRequest<'a> {
|
||||
pub(crate) command: &'a [String],
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) sandbox_policy: &'a SandboxPolicy,
|
||||
pub(crate) permission_profile: PermissionProfile,
|
||||
pub(crate) file_system_sandbox_policy: &'a FileSystemSandboxPolicy,
|
||||
pub(crate) sandbox_cwd: &'a Path,
|
||||
pub(crate) sandbox_permissions: SandboxPermissions,
|
||||
pub(crate) prefix_rule: Option<Vec<String>>,
|
||||
}
|
||||
@@ -238,8 +239,9 @@ impl ExecPolicyManager {
|
||||
let ExecApprovalRequest {
|
||||
command,
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
} = req;
|
||||
@@ -252,8 +254,9 @@ impl ExecPolicyManager {
|
||||
let exec_policy_fallback = |cmd: &[String]| {
|
||||
render_decision_for_unmatched_command(
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
&permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
cmd,
|
||||
sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
@@ -580,8 +583,9 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
|
||||
/// If a command is not matched by any execpolicy rule, derive a [`Decision`].
|
||||
pub fn render_decision_for_unmatched_command(
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
command: &[String],
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
used_complex_parsing: bool,
|
||||
@@ -592,8 +596,12 @@ pub fn render_decision_for_unmatched_command(
|
||||
|
||||
// On Windows, ReadOnly sandbox is not a real sandbox, so special-case it
|
||||
// here.
|
||||
let environment_lacks_sandbox_protections =
|
||||
cfg!(windows) && matches!(sandbox_policy, SandboxPolicy::ReadOnly { .. });
|
||||
let environment_lacks_sandbox_protections = cfg!(windows)
|
||||
&& profile_is_managed_read_only(
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_cwd,
|
||||
);
|
||||
|
||||
// If the command is flagged as dangerous or we have no sandbox protection,
|
||||
// we should never allow it to run without approval.
|
||||
@@ -605,8 +613,8 @@ pub fn render_decision_for_unmatched_command(
|
||||
return match approval_policy {
|
||||
AskForApproval::Never => {
|
||||
let sandbox_is_explicitly_disabled = matches!(
|
||||
sandbox_policy,
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||
permission_profile,
|
||||
PermissionProfile::Disabled | PermissionProfile::External { .. }
|
||||
);
|
||||
if sandbox_is_explicitly_disabled {
|
||||
// If the sandbox is explicitly disabled, we should allow the command to run
|
||||
@@ -670,6 +678,22 @@ pub fn render_decision_for_unmatched_command(
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_is_managed_read_only(
|
||||
permission_profile: &PermissionProfile,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_cwd: &Path,
|
||||
) -> bool {
|
||||
matches!(permission_profile, PermissionProfile::Managed { .. })
|
||||
&& matches!(
|
||||
file_system_sandbox_policy.kind,
|
||||
FileSystemSandboxKind::Restricted
|
||||
)
|
||||
&& !file_system_sandbox_policy.has_full_disk_write_access()
|
||||
&& file_system_sandbox_policy
|
||||
.get_writable_roots_with_cwd(sandbox_cwd)
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
fn default_policy_path(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(RULES_DIR_NAME).join(DEFAULT_POLICY_FILE)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::config_loader::ConfigLayerEntry;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::ConfigLayerStackOrdering;
|
||||
use crate::config_loader::ConfigRequirements;
|
||||
use crate::config_loader::ConfigRequirementsToml;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::ConfigRequirements;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::RequirementsExecPolicy;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::config_toml::ProjectConfig;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
@@ -108,6 +110,10 @@ fn read_only_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
}])
|
||||
}
|
||||
|
||||
fn workspace_write_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::from_legacy_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())
|
||||
}
|
||||
|
||||
fn unrestricted_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::unrestricted()
|
||||
}
|
||||
@@ -116,6 +122,10 @@ fn external_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
|
||||
FileSystemSandboxPolicy::external_sandbox()
|
||||
}
|
||||
|
||||
fn permission_profile_from_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile {
|
||||
PermissionProfile::from_legacy_sandbox_policy(sandbox_policy)
|
||||
}
|
||||
|
||||
async fn test_config() -> (TempDir, Config) {
|
||||
let home = TempDir::new().expect("create temp dir");
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
@@ -954,8 +964,9 @@ fn unmatched_granular_policy_still_prompts_for_restricted_sandbox_escalation() {
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
&permission_profile_from_sandbox_policy(&SandboxPolicy::new_read_only_policy()),
|
||||
&read_only_file_system_sandbox_policy(),
|
||||
Path::new("/tmp"),
|
||||
&command,
|
||||
SandboxPermissions::RequireEscalated,
|
||||
/*used_complex_parsing*/ false,
|
||||
@@ -972,8 +983,9 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() {
|
||||
Decision::Prompt,
|
||||
render_decision_for_unmatched_command(
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&PermissionProfile::Disabled,
|
||||
&restricted_file_system_policy,
|
||||
Path::new("/tmp"),
|
||||
&command,
|
||||
SandboxPermissions::RequireEscalated,
|
||||
/*used_complex_parsing*/ false,
|
||||
@@ -981,6 +993,65 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_cwd_write_profile_is_not_read_only() {
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
assert!(!profile_is_managed_read_only(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/tmp/project")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_unresolvable_write_profile_is_still_read_only() {
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::unknown(
|
||||
":future_special_path",
|
||||
/*subpath*/ None,
|
||||
),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
]);
|
||||
let permission_profile = PermissionProfile::from_runtime_permissions(
|
||||
&file_system_sandbox_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
assert!(profile_is_managed_read_only(
|
||||
&permission_profile,
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/tmp/project")
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_requirement_prompts_for_inline_additional_permissions_under_on_request() {
|
||||
assert_exec_approval_requirement_for_command(
|
||||
@@ -1058,8 +1129,11 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision()
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1095,8 +1169,11 @@ async fn mixed_rule_and_sandbox_prompt_rejects_when_granular_rules_are_disabled(
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1119,8 +1196,11 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1144,8 +1224,11 @@ async fn empty_bash_lc_script_falls_back_to_original_command() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1173,8 +1256,11 @@ async fn whitespace_bash_lc_script_falls_back_to_original_command() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1202,8 +1288,11 @@ async fn request_rule_uses_prefix_rule() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
})
|
||||
@@ -1234,8 +1323,9 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands(
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
})
|
||||
@@ -1273,8 +1363,9 @@ async fn heuristics_apply_when_other_commands_match_policy() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1498,7 +1589,7 @@ prefix_rule(pattern=["cat"], decision="allow")
|
||||
command: command.clone(),
|
||||
approval_policy,
|
||||
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
},
|
||||
@@ -1759,8 +1850,11 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &sneaky_command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1783,8 +1877,11 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &dangerous_command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1803,8 +1900,11 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &dangerous_command,
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
permission_profile: permission_profile_from_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
file_system_sandbox_policy: &read_only_file_system_sandbox_policy(),
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
})
|
||||
@@ -1897,12 +1997,14 @@ async fn assert_exec_approval_requirement_for_command(
|
||||
None => Arc::new(Policy::empty()),
|
||||
};
|
||||
|
||||
let permission_profile = permission_profile_from_sandbox_policy(&sandbox_policy);
|
||||
let requirement = ExecPolicyManager::new(policy)
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy,
|
||||
sandbox_policy: &sandbox_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy: &file_system_sandbox_policy,
|
||||
sandbox_cwd: Path::new("/tmp"),
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
@@ -346,6 +347,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result
|
||||
|
||||
let cwd = codex_utils_absolute_path::AbsolutePathBuf::current_dir()?;
|
||||
let sandbox_policy = SandboxPolicy::DangerFullAccess;
|
||||
let permission_profile = PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy);
|
||||
let output = process_exec_tool_call(
|
||||
ExecParams {
|
||||
command,
|
||||
@@ -360,9 +362,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result
|
||||
justification: None,
|
||||
arg0: None,
|
||||
},
|
||||
&sandbox_policy,
|
||||
&FileSystemSandboxPolicy::from(&sandbox_policy),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
&permission_profile,
|
||||
&cwd,
|
||||
&None,
|
||||
/*use_legacy_landlock*/ false,
|
||||
@@ -1021,11 +1021,10 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> {
|
||||
tokio::time::sleep(Duration::from_millis(1_000)).await;
|
||||
cancel_tx.cancel();
|
||||
});
|
||||
let permission_profile = PermissionProfile::Disabled;
|
||||
let result = process_exec_tool_call(
|
||||
params,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
&FileSystemSandboxPolicy::from(&SandboxPolicy::DangerFullAccess),
|
||||
NetworkSandboxPolicy::Enabled,
|
||||
&permission_profile,
|
||||
&cwd,
|
||||
&None,
|
||||
/*use_legacy_landlock*/ false,
|
||||
|
||||
@@ -45,6 +45,8 @@ pub(crate) const GUARDIAN_REVIEW_TIMEOUT: Duration = Duration::from_secs(90);
|
||||
pub(crate) const GUARDIAN_REVIEWER_NAME: &str = "guardian";
|
||||
pub(crate) const MAX_CONSECUTIVE_GUARDIAN_DENIALS_PER_TURN: u32 = 3;
|
||||
pub(crate) const MAX_TOTAL_GUARDIAN_DENIALS_PER_TURN: u32 = 10;
|
||||
pub(crate) const AUTO_REVIEW_DENIED_ACTION_APPROVAL_DEVELOPER_PREFIX: &str =
|
||||
"The user has manually approved a specific action that was previously `Rejected`.";
|
||||
const GUARDIAN_MAX_MESSAGE_TRANSCRIPT_TOKENS: usize = 10_000;
|
||||
const GUARDIAN_MAX_TOOL_TRANSCRIPT_TOKENS: usize = 10_000;
|
||||
const GUARDIAN_MAX_MESSAGE_ENTRY_TOKENS: usize = 2_000;
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_utils_output_truncation::approx_bytes_for_tokens;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use codex_utils_output_truncation::approx_tokens_from_byte_count;
|
||||
|
||||
use super::AUTO_REVIEW_DENIED_ACTION_APPROVAL_DEVELOPER_PREFIX;
|
||||
use super::GUARDIAN_MAX_MESSAGE_ENTRY_TOKENS;
|
||||
use super::GUARDIAN_MAX_MESSAGE_TRANSCRIPT_TOKENS;
|
||||
use super::GUARDIAN_MAX_TOOL_ENTRY_TOKENS;
|
||||
@@ -33,6 +34,7 @@ pub(crate) struct GuardianTranscriptEntry {
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum GuardianTranscriptEntryKind {
|
||||
Developer,
|
||||
User,
|
||||
Assistant,
|
||||
Tool(String),
|
||||
@@ -41,6 +43,7 @@ pub(crate) enum GuardianTranscriptEntryKind {
|
||||
impl GuardianTranscriptEntryKind {
|
||||
fn role(&self) -> &str {
|
||||
match self {
|
||||
Self::Developer => "developer",
|
||||
Self::User => "user",
|
||||
Self::Assistant => "assistant",
|
||||
Self::Tool(role) => role.as_str(),
|
||||
@@ -361,6 +364,18 @@ pub(crate) fn collect_guardian_transcript_entries(
|
||||
content_entry(GuardianTranscriptEntryKind::User, content)
|
||||
}
|
||||
}
|
||||
ResponseItem::Message { role, content, .. } if role == "developer" => {
|
||||
content_items_to_text(content).and_then(|text| {
|
||||
// Preserve only the explicit auto-review approval marker for
|
||||
// Guardian context; other developer messages are intentionally
|
||||
// excluded from the review transcript.
|
||||
text.starts_with(AUTO_REVIEW_DENIED_ACTION_APPROVAL_DEVELOPER_PREFIX)
|
||||
.then_some(GuardianTranscriptEntry {
|
||||
kind: GuardianTranscriptEntryKind::Developer,
|
||||
text,
|
||||
})
|
||||
})
|
||||
}
|
||||
ResponseItem::Message { role, content, .. } if role == "assistant" => {
|
||||
content_entry(GuardianTranscriptEntryKind::Assistant, content)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_analytics::GuardianReviewAnalyticsResult;
|
||||
use codex_analytics::GuardianReviewSessionKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
@@ -843,8 +844,16 @@ pub(crate) fn build_guardian_review_session_config(
|
||||
);
|
||||
guardian_config.developer_instructions = None;
|
||||
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
|
||||
guardian_config.permissions.sandbox_policy =
|
||||
Constrained::allow_only(SandboxPolicy::new_read_only_policy());
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
guardian_config.permissions.permission_profile = Constrained::allow_only(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
|
||||
);
|
||||
guardian_config
|
||||
.permissions
|
||||
.set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path())
|
||||
.map_err(|err| {
|
||||
anyhow::anyhow!("guardian review session could not set sandbox policy: {err}")
|
||||
})?;
|
||||
guardian_config.include_apps_instructions = false;
|
||||
guardian_config
|
||||
.mcp_servers
|
||||
@@ -864,7 +873,7 @@ pub(crate) fn build_guardian_review_session_config(
|
||||
guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
|
||||
live_network_config,
|
||||
network_constraints,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
guardian_config.permissions.permission_profile.get(),
|
||||
)?);
|
||||
}
|
||||
for feature in [
|
||||
|
||||
@@ -5,18 +5,18 @@ use crate::config::Constrained;
|
||||
use crate::config::ManagedFeatures;
|
||||
use crate::config::NetworkProxySpec;
|
||||
use crate::config::test_config;
|
||||
use crate::config_loader::ConfigLayerStack;
|
||||
use crate::config_loader::FeatureRequirementsToml;
|
||||
use crate::config_loader::NetworkConstraints;
|
||||
use crate::config_loader::NetworkDomainPermissionToml;
|
||||
use crate::config_loader::NetworkDomainPermissionsToml;
|
||||
use crate::config_loader::RequirementSource;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::guardian::approval_request::guardian_request_target_item_id;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::test_support;
|
||||
use codex_analytics::GuardianApprovalRequestSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::FeatureRequirementsToml;
|
||||
use codex_config::NetworkConstraints;
|
||||
use codex_config::NetworkDomainPermissionToml;
|
||||
use codex_config::NetworkDomainPermissionsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
@@ -27,6 +27,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
@@ -175,7 +176,6 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
|
||||
text: "Please check the repo visibility and push the docs fix if needed."
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::FunctionCall {
|
||||
@@ -198,7 +198,6 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
|
||||
text: "The repo is public; I now need approval to push the docs fix."
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -342,7 +341,6 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Please also push the second docs fix.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -351,7 +349,6 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need approval for the second push.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -475,7 +472,6 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Compacted retained user request.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -484,7 +480,6 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "Compacted summary of earlier guardian context.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -500,7 +495,6 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Please push after the compaction.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -509,7 +503,6 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need approval for the post-compaction push.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -558,7 +551,6 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -567,7 +559,6 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "hello".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -584,6 +575,40 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message() {
|
||||
let approval_text =
|
||||
format!("{AUTO_REVIEW_DENIED_ACTION_APPROVAL_DEVELOPER_PREFIX}\n\nApproved action:\n{{}}");
|
||||
let items = vec![
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "ordinary developer context".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: approval_text.clone(),
|
||||
}],
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
|
||||
let entries = collect_guardian_transcript_entries(&items);
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![GuardianTranscriptEntry {
|
||||
kind: GuardianTranscriptEntryKind::Developer,
|
||||
text: approval_text,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
|
||||
let items = vec![
|
||||
@@ -593,7 +618,6 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "check the repo".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::FunctionCall {
|
||||
@@ -615,7 +639,6 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need to push a fix".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
@@ -1356,7 +1379,6 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Please push the second docs fix too.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -1365,7 +1387,6 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need approval for the second docs fix.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -1402,7 +1423,6 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Please push the third docs fix too.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -1411,7 +1431,6 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need approval for the third docs fix.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -1789,7 +1808,6 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Please inspect pending changes before pushing.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -1798,7 +1816,6 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need approval to run git diff.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -1858,7 +1875,6 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Now inspect whether pushing is safe.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
@@ -1867,7 +1883,6 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
content: vec![ContentItem::OutputText {
|
||||
text: "I need approval to push after the diff check.".to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
],
|
||||
@@ -1942,7 +1957,7 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
parent_config.permissions.sandbox_policy.get(),
|
||||
parent_config.permissions.permission_profile.get(),
|
||||
)
|
||||
.expect("network proxy spec");
|
||||
parent_config.permissions.network = Some(network.clone());
|
||||
@@ -1969,8 +1984,10 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
|
||||
Constrained::allow_only(AskForApproval::Never)
|
||||
);
|
||||
assert_eq!(
|
||||
guardian_config.permissions.sandbox_policy,
|
||||
Constrained::allow_only(SandboxPolicy::new_read_only_policy())
|
||||
guardian_config.permissions.permission_profile,
|
||||
Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2007,7 +2024,7 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
NetworkProxySpec::from_config_and_constraints(
|
||||
parent_network,
|
||||
/*requirements*/ None,
|
||||
parent_config.permissions.sandbox_policy.get(),
|
||||
parent_config.permissions.permission_profile.get(),
|
||||
)
|
||||
.expect("parent network proxy spec"),
|
||||
);
|
||||
@@ -2032,7 +2049,9 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
|
||||
NetworkProxySpec::from_config_and_constraints(
|
||||
live_network,
|
||||
/*requirements*/ None,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
&PermissionProfile::from_legacy_sandbox_policy(
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
),
|
||||
)
|
||||
.expect("live network proxy spec")
|
||||
)
|
||||
@@ -2122,7 +2141,7 @@ async fn guardian_review_session_config_uses_requirements_guardian_policy_config
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
Vec::new(),
|
||||
Default::default(),
|
||||
crate::config_loader::ConfigRequirementsToml {
|
||||
codex_config::ConfigRequirementsToml {
|
||||
guardian_policy_config: Some(
|
||||
" Use the workspace-managed guardian policy. ".to_string(),
|
||||
),
|
||||
|
||||
@@ -2,9 +2,8 @@ use crate::spawn::SpawnChildRequest;
|
||||
use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
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;
|
||||
@@ -18,15 +17,15 @@ 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 the legacy [`SandboxPolicy`] plus
|
||||
/// split filesystem/network policies as JSON so the helper can migrate
|
||||
/// incrementally without breaking older call sites.
|
||||
/// 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.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn spawn_command_under_linux_sandbox<P>(
|
||||
codex_linux_sandbox_exe: P,
|
||||
command: Vec<String>,
|
||||
command_cwd: AbsolutePathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
permission_profile: &PermissionProfile,
|
||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||
use_legacy_landlock: bool,
|
||||
stdio_policy: StdioPolicy,
|
||||
@@ -36,15 +35,18 @@ pub async fn spawn_command_under_linux_sandbox<P>(
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
|
||||
sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
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 network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy);
|
||||
let args = create_linux_sandbox_command_args_for_policies(
|
||||
command,
|
||||
command_cwd.as_path(),
|
||||
sandbox_policy,
|
||||
&sandbox_policy,
|
||||
&file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user