permissions: make profiles represent enforcement

This commit is contained in:
Michael Bolin
2026-04-23 17:08:51 -07:00
parent a9f75e5cda
commit 6c5b77a080
73 changed files with 2091 additions and 890 deletions
+2 -1
View File
@@ -837,7 +837,8 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin
"env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names
"size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true
"permissionProfile": { // optional; defaults to user config
"fileSystem": { "entries": [
"type": "managed",
"fileSystem": { "type": "restricted", "entries": [
{ "path": { "type": "special", "value": { "kind": "root" } }, "access": "read" },
{ "path": { "type": "special", "value": { "kind": "current_working_directory" } }, "access": "write" }
] },
@@ -122,7 +122,7 @@ use codex_protocol::ThreadId;
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse;
use codex_protocol::items::parse_hook_prompt_message;
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
use codex_protocol::protocol::Event;
@@ -2719,7 +2719,7 @@ fn request_permissions_response_from_client_result(
strict_auto_review: false,
});
}
let granted_permissions: CorePermissionProfile = response.permissions.into();
let granted_permissions: CoreAdditionalPermissionProfile = response.permissions.into();
let permissions = if granted_permissions.is_empty() {
CoreRequestPermissionProfile::default()
} else {
@@ -314,7 +314,6 @@ use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::AgentStatus;
use codex_protocol::protocol::ConversationAudioParams;
@@ -2361,24 +2360,8 @@ impl CodexMessageProcessor {
file_system_sandbox_policy: &mut FileSystemSandboxPolicy,
configured_file_system_sandbox_policy: &FileSystemSandboxPolicy,
) {
if file_system_sandbox_policy.glob_scan_max_depth.is_none() {
file_system_sandbox_policy.glob_scan_max_depth =
configured_file_system_sandbox_policy.glob_scan_max_depth;
}
for deny_entry in configured_file_system_sandbox_policy
.entries
.iter()
.filter(|entry| entry.access == FileSystemAccessMode::None)
{
if !file_system_sandbox_policy
.entries
.iter()
.any(|entry| entry == deny_entry)
{
file_system_sandbox_policy.entries.push(deny_entry.clone());
}
}
file_system_sandbox_policy
.preserve_deny_read_restrictions_from(configured_file_system_sandbox_policy);
}
async fn command_exec_write(
@@ -2800,10 +2783,8 @@ impl CodexMessageProcessor {
/*has_in_progress_turn*/ false,
);
let permission_profile = thread_response_permission_profile(
&config_snapshot.sandbox_policy,
config_snapshot.permission_profile,
);
let permission_profile =
thread_response_permission_profile(config_snapshot.permission_profile);
let response = ThreadStartResponse {
thread: thread.clone(),
@@ -4608,7 +4589,6 @@ impl CodexMessageProcessor {
/*has_live_in_progress_turn*/ false,
);
let permission_profile = thread_response_permission_profile(
&session_configured.sandbox_policy,
codex_thread.config_snapshot().await.permission_profile,
);
@@ -5279,7 +5259,6 @@ impl CodexMessageProcessor {
/*has_in_progress_turn*/ false,
);
let permission_profile = thread_response_permission_profile(
&session_configured.sandbox_policy,
forked_thread.config_snapshot().await.permission_profile,
);
@@ -8794,8 +8773,7 @@ async fn handle_pending_thread_resume_request(
..
} = pending.config_snapshot;
let instruction_sources = pending.instruction_sources;
let permission_profile =
thread_response_permission_profile(&sandbox_policy, permission_profile);
let permission_profile = thread_response_permission_profile(permission_profile);
let response = ThreadResumeResponse {
thread,
@@ -9925,17 +9903,9 @@ fn with_thread_spawn_agent_metadata(
}
fn thread_response_permission_profile(
sandbox_policy: &codex_protocol::protocol::SandboxPolicy,
permission_profile: codex_protocol::models::PermissionProfile,
) -> Option<codex_app_server_protocol::PermissionProfile> {
match sandbox_policy {
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
| codex_protocol::protocol::SandboxPolicy::ReadOnly { .. }
| codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => {
Some(permission_profile.into())
}
codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => None,
}
Some(permission_profile.into())
}
fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) -> bool {
@@ -10252,6 +10222,7 @@ mod tests {
use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::protocol::AskForApproval;
@@ -10457,25 +10428,28 @@ mod tests {
}
#[test]
fn thread_response_permission_profile_omits_external_sandbox() {
fn thread_response_permission_profile_preserves_enforcement() {
let cwd = test_path_buf("/tmp").abs();
let profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
cwd.as_path(),
);
assert_eq!(
thread_response_permission_profile(
let full_access_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
cwd.as_path(),
);
let external_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
},
profile.clone(),
),
None
cwd.as_path(),
);
assert_eq!(
thread_response_permission_profile(external_profile.clone()),
Some(external_profile.into())
);
assert_eq!(
thread_response_permission_profile(&SandboxPolicy::DangerFullAccess, profile.clone()),
Some(profile.into())
thread_response_permission_profile(full_access_profile.clone()),
Some(full_access_profile.into())
);
}
@@ -256,17 +256,18 @@ async fn command_exec_permission_profile_cwd_uses_command_cwd() -> Result<()> {
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let mut permission_profile = root_read_only_permission_profile();
permission_profile
.file_system
.as_mut()
.expect("root read-only helper should include filesystem permissions")
.entries
.push(FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Write,
});
let PermissionProfile::Managed { file_system, .. } = &mut permission_profile else {
panic!("root read-only helper should use managed permissions");
};
let PermissionProfileFileSystemPermissions::Restricted { entries, .. } = file_system else {
panic!("root read-only helper should use restricted filesystem permissions");
};
entries.push(FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Write,
});
let command_request_id = mcp
.send_command_exec_request(CommandExecParams {
@@ -1061,11 +1062,9 @@ fn decode_delta_notification(
}
fn root_read_only_permission_profile() -> PermissionProfile {
PermissionProfile {
network: Some(PermissionProfileNetworkPermissions {
enabled: Some(false),
}),
file_system: Some(PermissionProfileFileSystemPermissions {
PermissionProfile::Managed {
network: PermissionProfileNetworkPermissions { enabled: false },
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
@@ -1073,7 +1072,7 @@ fn root_read_only_permission_profile() -> PermissionProfile {
access: FileSystemAccessMode::Read,
}],
glob_scan_max_depth: None,
}),
},
}
}
@@ -39,6 +39,7 @@ use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PatchChangeKind;
use codex_app_server_protocol::PermissionProfile;
use codex_app_server_protocol::PermissionProfileFileSystemPermissions;
use codex_app_server_protocol::PermissionProfileNetworkPermissions;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ServerRequestResolvedNotification;
@@ -779,9 +780,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
permission_profile: Some(PermissionProfile {
network: None,
file_system: Some(PermissionProfileFileSystemPermissions {
permission_profile: Some(PermissionProfile::Managed {
network: PermissionProfileNetworkPermissions { enabled: false },
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: unsupported_write_root,
@@ -789,7 +790,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
}),
},
}),
..Default::default()
})