mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Resolve permission requests in the executor context (#42146)
## Why `request_permissions` paths and grants need to be evaluated against the selected executor environment, including its path convention, home directory, workspace roots, and temporary directories. ## What changed - Resolve relative and home-relative permission paths using the executor context, reject mismatched path conventions and lossy paths, and support legacy `read` and `write` path lists. - Move grant intersection into core so requested and granted permissions use the originating environment's sandbox context. Preserve deny entries conservatively when a special path cannot be resolved. - Keep the full originating environment with pending permission requests so delayed responses are normalized against the same context. ## Testing - Cover POSIX, Windows, UNC, relative, and home-relative path resolution and invalid path contexts. - Verify end-to-end app-server grants are limited to the requested workspace scope and unresolved temporary-directory denies are preserved. GitOrigin-RevId: 730a2aacd391262e92a6314f3a5b6c262e3dca10
This commit is contained in:
1 parent
86b7d12742
commit
cd8bd62c6e
10 files changed
+492
-307
No files matched your search
@@ -2,7 +2,6 @@ use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
@@ -121,6 +120,7 @@ use codex_protocol::models::SandboxEnforcement;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::HasLegacyEvent;
|
||||
@@ -156,7 +156,7 @@ use codex_rollout::state_db;
|
||||
use codex_rollout_trace::AgentResultTracePayload;
|
||||
use codex_rollout_trace::ThreadStartedTraceMetadata;
|
||||
use codex_rollout_trace::ThreadTraceContext;
|
||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
|
||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles_with_context;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_terminal_detection::user_agent;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
@@ -2757,12 +2757,27 @@ impl Session {
|
||||
step_context: &Arc<StepContext>,
|
||||
call_id: String,
|
||||
args: RequestPermissionsArgs,
|
||||
environment: TurnEnvironmentSelection,
|
||||
environment_selection: TurnEnvironmentSelection,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<RequestPermissionsResponse> {
|
||||
let turn_context = &step_context.turn;
|
||||
let approval_policy = step_context.settings.approval_policy();
|
||||
let approvals_reviewer = step_context.settings.approvals_reviewer();
|
||||
let Some(environment) = step_context
|
||||
.environments
|
||||
.turn_environments()
|
||||
.find(|candidate| {
|
||||
candidate.selection.environment_id == environment_selection.environment_id
|
||||
})
|
||||
.cloned()
|
||||
else {
|
||||
return Some(RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
};
|
||||
let cwd = environment.cwd();
|
||||
match approval_policy {
|
||||
AskForApproval::Never => {
|
||||
return Some(RequestPermissionsResponse {
|
||||
@@ -2786,19 +2801,14 @@ impl Session {
|
||||
}
|
||||
|
||||
let requested_permissions = args.permissions;
|
||||
// TODO(anp): Migrate request_permissions to support paths from foreign environments.
|
||||
let Ok(native_environment_cwd) = environment.cwd.to_abs_path() else {
|
||||
warn!(
|
||||
cwd = %environment.cwd,
|
||||
"request_permissions requires a cwd native to the Codex host"
|
||||
);
|
||||
let sandbox_context = environment.sandbox_context(/*additional_permissions*/ None);
|
||||
let Some(context) = sandbox_context.policy_context() else {
|
||||
return Some(RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
};
|
||||
|
||||
if crate::guardian::routes_approval_policy_to_guardian(approval_policy, approvals_reviewer)
|
||||
{
|
||||
let originating_turn_state = {
|
||||
@@ -2868,17 +2878,30 @@ impl Session {
|
||||
let response = Self::normalize_request_permissions_response(
|
||||
requested_permissions,
|
||||
response,
|
||||
native_environment_cwd.as_path(),
|
||||
&context,
|
||||
);
|
||||
self.record_granted_request_permissions_for_turn(
|
||||
&response,
|
||||
&environment.environment_id,
|
||||
&environment.selection.environment_id,
|
||||
originating_turn_state.as_ref(),
|
||||
)
|
||||
.await;
|
||||
return Some(response);
|
||||
}
|
||||
|
||||
// The interactive approval event remains host-native until its public
|
||||
// app-server/TUI boundary migrates in the next stack stage.
|
||||
let Ok(native_cwd) = cwd.to_abs_path() else {
|
||||
warn!(
|
||||
cwd = %cwd,
|
||||
"request_permissions interactive approval requires a cwd native to the Codex host"
|
||||
);
|
||||
return Some(RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
};
|
||||
let _elicitation = self.services.elicitations.register();
|
||||
let (tx_response, rx_response) = oneshot::channel();
|
||||
let prev_entry = {
|
||||
@@ -2905,11 +2928,11 @@ impl Session {
|
||||
let event = EventMsg::RequestPermissions(RequestPermissionsEvent {
|
||||
call_id: call_id.clone(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
environment_id: Some(environment.environment_id.clone()),
|
||||
environment_id: Some(environment.selection.environment_id.clone()),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
reason: args.reason,
|
||||
permissions: requested_permissions,
|
||||
cwd: Some(native_environment_cwd),
|
||||
cwd: Some(native_cwd),
|
||||
});
|
||||
self.send_event(turn_context.as_ref(), event).await;
|
||||
tokio::select! {
|
||||
@@ -3020,29 +3043,25 @@ impl Session {
|
||||
};
|
||||
match entry {
|
||||
Some(entry) => {
|
||||
// TODO(anp): Migrate request_permissions to support paths from foreign environments.
|
||||
let response = match entry.environment.cwd.to_abs_path() {
|
||||
Ok(native_environment_cwd) => Self::normalize_request_permissions_response(
|
||||
let sandbox_context = entry
|
||||
.environment
|
||||
.sandbox_context(/*additional_permissions*/ None);
|
||||
let response = if let Some(context) = sandbox_context.policy_context() {
|
||||
Self::normalize_request_permissions_response(
|
||||
entry.requested_permissions,
|
||||
response,
|
||||
native_environment_cwd.as_path(),
|
||||
),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
cwd = %entry.environment.cwd,
|
||||
%err,
|
||||
"request_permissions requires a cwd native to the Codex host"
|
||||
);
|
||||
RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
}
|
||||
&context,
|
||||
)
|
||||
} else {
|
||||
RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
}
|
||||
};
|
||||
self.record_granted_request_permissions_for_turn(
|
||||
&response,
|
||||
&entry.environment.environment_id,
|
||||
&entry.environment.selection.environment_id,
|
||||
originating_turn_state.as_ref(),
|
||||
)
|
||||
.await;
|
||||
@@ -3057,7 +3076,7 @@ impl Session {
|
||||
fn normalize_request_permissions_response(
|
||||
requested_permissions: RequestPermissionProfile,
|
||||
response: RequestPermissionsResponse,
|
||||
cwd: &Path,
|
||||
context: &FileSystemSandboxPolicyContext<'_>,
|
||||
) -> RequestPermissionsResponse {
|
||||
if response.strict_auto_review && matches!(response.scope, PermissionGrantScope::Session) {
|
||||
return RequestPermissionsResponse {
|
||||
@@ -3072,10 +3091,10 @@ impl Session {
|
||||
}
|
||||
|
||||
RequestPermissionsResponse {
|
||||
permissions: intersect_permission_profiles(
|
||||
permissions: intersect_permission_profiles_with_context(
|
||||
requested_permissions.into(),
|
||||
response.permissions.into(),
|
||||
cwd,
|
||||
context,
|
||||
)
|
||||
.into(),
|
||||
scope: response.scope,
|
||||
|
||||
@@ -79,6 +79,7 @@ use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicyContext;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
@@ -7244,6 +7245,13 @@ fn strict_auto_review_session_scope_grants_no_permissions() {
|
||||
..RequestPermissionProfile::default()
|
||||
};
|
||||
|
||||
let cwd = PathUri::parse("file:///tmp").expect("test cwd should be valid");
|
||||
let context = FileSystemSandboxPolicyContext {
|
||||
cwd: &cwd,
|
||||
workspace_roots: &[],
|
||||
user_home_dir: None,
|
||||
temporary_directories: None,
|
||||
};
|
||||
let response = Session::normalize_request_permissions_response(
|
||||
requested_permissions.clone(),
|
||||
codex_protocol::request_permissions::RequestPermissionsResponse {
|
||||
@@ -7251,7 +7259,7 @@ fn strict_auto_review_session_scope_grants_no_permissions() {
|
||||
scope: PermissionGrantScope::Session,
|
||||
strict_auto_review: true,
|
||||
},
|
||||
std::path::Path::new("/tmp"),
|
||||
&context,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -7355,7 +7363,7 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_permissions_tool_resolves_relative_paths_against_selected_environment() {
|
||||
async fn request_permissions_tool_resolves_legacy_paths_against_selected_environment() {
|
||||
let (session, mut turn_context, rx) = make_session_and_context_with_rx().await;
|
||||
*session.active_turn.lock().await = Some(ActiveTurn::default());
|
||||
let environment_cwd = {
|
||||
@@ -7363,6 +7371,7 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
let legacy_cwd = turn_context.cwd.clone();
|
||||
legacy_cwd.join("request-permissions-environment")
|
||||
};
|
||||
let environment_home = environment_cwd.join("home");
|
||||
std::fs::create_dir_all(environment_cwd.as_path()).expect("create environment cwd");
|
||||
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("single thread settings ref");
|
||||
Arc::make_mut(&mut turn_context_mut.config)
|
||||
@@ -7382,8 +7391,9 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
.expect("primary environment")
|
||||
.clone();
|
||||
let current_environment_config = current_environment.config().clone();
|
||||
turn_context_mut.environments.environments[0] =
|
||||
TurnEnvironmentState::Ready(TurnEnvironment::new(
|
||||
let environment = TurnEnvironment {
|
||||
user_home_dir: Some(PathUri::from_abs_path(&environment_home)),
|
||||
..TurnEnvironment::new(
|
||||
TurnEnvironmentSelection {
|
||||
environment_id: "remote".to_string(),
|
||||
cwd: PathUri::from_abs_path(&environment_cwd),
|
||||
@@ -7393,7 +7403,9 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
current_environment.config_origin,
|
||||
current_environment.environment,
|
||||
current_environment.shell,
|
||||
));
|
||||
)
|
||||
};
|
||||
turn_context_mut.environments.environments[0] = TurnEnvironmentState::Ready(environment);
|
||||
|
||||
let call_id = "call-1".to_string();
|
||||
let handler = RequestPermissionsHandler;
|
||||
@@ -7422,13 +7434,8 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
"reason": "need write",
|
||||
"permissions": {
|
||||
"file_system": {
|
||||
"entries": [{
|
||||
"path": {
|
||||
"type": "path",
|
||||
"path": "relative.txt",
|
||||
},
|
||||
"access": "write",
|
||||
}],
|
||||
"read": null,
|
||||
"write": ["relative.txt", "~/home-relative.txt"],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -7447,16 +7454,13 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
panic!("expected request_permissions event");
|
||||
};
|
||||
let expected_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: environment_cwd.join("relative.txt").into(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
missing_path_behavior: None,
|
||||
}],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![
|
||||
environment_cwd.join("relative.txt"),
|
||||
environment_home.join("home-relative.txt"),
|
||||
]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(request.environment_id.as_deref(), Some("remote"));
|
||||
@@ -7479,9 +7483,22 @@ async fn request_permissions_tool_resolves_relative_paths_against_selected_envir
|
||||
.expect("request_permissions handler should succeed");
|
||||
}
|
||||
|
||||
#[test_case("missing", "unknown turn environment id `missing`"; "unknown environment")]
|
||||
#[test_case("local", "permission path cannot be represented losslessly"; "lossy path")]
|
||||
#[tokio::test]
|
||||
async fn request_permissions_tool_rejects_unknown_environment_id() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
async fn request_permissions_tool_rejects_invalid_requests(
|
||||
environment_id: &str,
|
||||
expected_error: &str,
|
||||
) {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
Arc::make_mut(&mut turn_context.config)
|
||||
.permissions
|
||||
.approval_policy = codex_config::Constrained::allow_any(AskForApproval::Never);
|
||||
let TurnEnvironmentState::Ready(environment) = &mut turn_context.environments.environments[0]
|
||||
else {
|
||||
panic!("turn environment should be ready");
|
||||
};
|
||||
environment.selection.cwd = PathUri::parse("file:///workspace/%FF").expect("non-UTF8 cwd");
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let step_context = StepContext::for_test(Arc::clone(&turn_context));
|
||||
let result = RequestPermissionsHandler
|
||||
@@ -7496,10 +7513,10 @@ async fn request_permissions_tool_rejects_unknown_environment_id() {
|
||||
source: ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: json!({
|
||||
"environment_id": "missing",
|
||||
"environment_id": environment_id,
|
||||
"permissions": {
|
||||
"network": {
|
||||
"enabled": true,
|
||||
"file_system": {
|
||||
"write": ["relative"],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -7509,9 +7526,9 @@ async fn request_permissions_tool_rejects_unknown_environment_id() {
|
||||
.await;
|
||||
|
||||
let Err(FunctionCallError::RespondToModel(output)) = result else {
|
||||
panic!("expected unknown environment id to be rejected");
|
||||
panic!("expected invalid request to be rejected");
|
||||
};
|
||||
assert_eq!(output, "unknown turn environment id `missing`");
|
||||
assert_eq!(output, expected_error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in new issue
Block a user