mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Key request-permission grants by environment (#25850)
## Stack 1. This PR (#25850) - Key request-permission grants by environment: stores and applies sticky permission grants per environment id. 2. #25858 - Add `environmentId` to `request_permissions`: lets the model target a selected environment and resolves relative permission paths against it. 3. #25862 - Propagate permission approval environment id: carries the selected environment id through approval events, app-server requests, TUI prompts, and delegate forwarding. 4. #25867 - Add remote request permissions integration coverage: verifies the selected remote environment across request, approval, grant reuse, and exec. #25858, #25862, and #25867 are stacked on this PR and should be reviewed after it. ## Why Multi-environment CCA turns can attach both local and remote executors, but request-permission grants were still effectively cwd-only. Pending permission requests tracked a cwd, while stored turn/session grants had no environment identity, so sticky grants could be reused through the wrong executor context. This makes the first permission-grant step environment-aware without changing the external `request_permissions` payload shape: omitted environment targeting remains bound to the primary turn environment. ## What Changed - Store turn- and session-scoped request-permission grants by `environment_id`. - Keep the selected `TurnEnvironmentSelection` with pending `request_permissions` calls so approval responses normalize and record grants against the same environment. - Resolve relative `request_permissions` file paths against the primary turn environment cwd instead of deprecated `turn.cwd`. - Apply sticky grants in `shell`, `exec_command`, and `apply_patch` by selected environment id while still using the actual tool cwd for cwd-relative permission materialization. - Update Guardian and request-permissions coverage for the environment-keyed grant behavior. ## Testing Not run locally. Added or updated focused coverage for: - `request_permission_grants_are_environment_keyed` - `request_permissions_tool_resolves_relative_paths_against_primary_environment` - related Guardian/request-permissions sticky grant tests
This commit is contained in:
@@ -2107,12 +2107,18 @@ impl Session {
|
||||
args: RequestPermissionsArgs,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<RequestPermissionsResponse> {
|
||||
self.request_permissions_for_cwd(
|
||||
let Some(turn_environment) = turn_context.environments.primary() else {
|
||||
return Some(RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
};
|
||||
self.request_permissions_for_environment(
|
||||
turn_context,
|
||||
call_id,
|
||||
args,
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.clone(),
|
||||
turn_environment.selection(),
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
@@ -2122,12 +2128,12 @@ impl Session {
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
)]
|
||||
pub(crate) async fn request_permissions_for_cwd(
|
||||
pub(crate) async fn request_permissions_for_environment(
|
||||
self: &Arc<Self>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
call_id: String,
|
||||
args: RequestPermissionsArgs,
|
||||
cwd: AbsolutePathBuf,
|
||||
environment: TurnEnvironmentSelection,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<RequestPermissionsResponse> {
|
||||
match turn_context.as_ref().approval_policy.value() {
|
||||
@@ -2221,10 +2227,11 @@ impl Session {
|
||||
let response = Self::normalize_request_permissions_response(
|
||||
requested_permissions,
|
||||
response,
|
||||
cwd.as_path(),
|
||||
environment.cwd.as_path(),
|
||||
);
|
||||
self.record_granted_request_permissions_for_turn(
|
||||
&response,
|
||||
&environment.environment_id,
|
||||
originating_turn_state.as_ref(),
|
||||
)
|
||||
.await;
|
||||
@@ -2242,7 +2249,7 @@ impl Session {
|
||||
PendingRequestPermissions {
|
||||
tx_response,
|
||||
requested_permissions: requested_permissions.clone(),
|
||||
cwd: cwd.clone(),
|
||||
environment: environment.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2259,7 +2266,7 @@ impl Session {
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
reason: args.reason,
|
||||
permissions: requested_permissions,
|
||||
cwd: Some(cwd),
|
||||
cwd: Some(environment.cwd),
|
||||
});
|
||||
self.send_event(turn_context.as_ref(), event).await;
|
||||
tokio::select! {
|
||||
@@ -2276,6 +2283,33 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn request_permissions_for_cwd(
|
||||
self: &Arc<Self>,
|
||||
turn_context: &Arc<TurnContext>,
|
||||
call_id: String,
|
||||
args: RequestPermissionsArgs,
|
||||
cwd: AbsolutePathBuf,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Option<RequestPermissionsResponse> {
|
||||
let Some(primary_environment) = turn_context.environments.primary() else {
|
||||
return Some(RequestPermissionsResponse {
|
||||
permissions: RequestPermissionProfile::default(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
};
|
||||
let mut environment = primary_environment.selection();
|
||||
environment.cwd = cwd;
|
||||
self.request_permissions_for_environment(
|
||||
turn_context,
|
||||
call_id,
|
||||
args,
|
||||
environment,
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
@@ -2370,10 +2404,11 @@ impl Session {
|
||||
let response = Self::normalize_request_permissions_response(
|
||||
entry.requested_permissions,
|
||||
response,
|
||||
entry.cwd.as_path(),
|
||||
entry.environment.cwd.as_path(),
|
||||
);
|
||||
self.record_granted_request_permissions_for_turn(
|
||||
&response,
|
||||
&entry.environment.environment_id,
|
||||
originating_turn_state.as_ref(),
|
||||
)
|
||||
.await;
|
||||
@@ -2417,6 +2452,7 @@ impl Session {
|
||||
async fn record_granted_request_permissions_for_turn(
|
||||
&self,
|
||||
response: &RequestPermissionsResponse,
|
||||
environment_id: &str,
|
||||
originating_turn_state: Option<&Arc<Mutex<crate::state::TurnState>>>,
|
||||
) {
|
||||
if response.permissions.is_empty() {
|
||||
@@ -2428,7 +2464,7 @@ impl Session {
|
||||
let mut ts = turn_state.lock().await;
|
||||
let permissions: AdditionalPermissionProfile =
|
||||
response.permissions.clone().into();
|
||||
ts.record_granted_permissions(permissions);
|
||||
ts.record_granted_permissions(environment_id, permissions);
|
||||
if response.strict_auto_review {
|
||||
ts.enable_strict_auto_review();
|
||||
}
|
||||
@@ -2436,7 +2472,10 @@ impl Session {
|
||||
}
|
||||
PermissionGrantScope::Session => {
|
||||
let mut state = self.state.lock().await;
|
||||
state.record_granted_permissions(response.permissions.clone().into());
|
||||
state.record_granted_permissions(
|
||||
environment_id,
|
||||
response.permissions.clone().into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2445,11 +2484,14 @@ impl Session {
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn reads must stay consistent with the matching turn state"
|
||||
)]
|
||||
pub(crate) async fn granted_turn_permissions(&self) -> Option<AdditionalPermissionProfile> {
|
||||
pub(crate) async fn granted_turn_permissions(
|
||||
&self,
|
||||
environment_id: &str,
|
||||
) -> Option<AdditionalPermissionProfile> {
|
||||
let active = self.active_turn.lock().await;
|
||||
let active = active.as_ref()?;
|
||||
let ts = active.turn_state.lock().await;
|
||||
ts.granted_permissions()
|
||||
ts.granted_permissions(environment_id)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
@@ -2465,9 +2507,12 @@ impl Session {
|
||||
ts.strict_auto_review_enabled()
|
||||
}
|
||||
|
||||
pub(crate) async fn granted_session_permissions(&self) -> Option<AdditionalPermissionProfile> {
|
||||
pub(crate) async fn granted_session_permissions(
|
||||
&self,
|
||||
environment_id: &str,
|
||||
) -> Option<AdditionalPermissionProfile> {
|
||||
let state = self.state.lock().await;
|
||||
state.granted_permissions()
|
||||
state.granted_permissions(environment_id)
|
||||
}
|
||||
|
||||
#[expect(
|
||||
|
||||
@@ -73,6 +73,7 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::CreateGoalHandler;
|
||||
use crate::tools::handlers::ExecCommandHandler;
|
||||
use crate::tools::handlers::RequestPermissionsHandler;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::UpdateGoalHandler;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
@@ -5155,7 +5156,12 @@ async fn notify_request_permissions_response_ignores_unmatched_call_id() {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(session.granted_turn_permissions().await, None);
|
||||
assert_eq!(
|
||||
session
|
||||
.granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -5182,16 +5188,84 @@ async fn record_granted_request_permissions_for_turn_uses_originating_turn() {
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
},
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID,
|
||||
Some(&originating_turn_state),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
originating_turn_state.lock().await.granted_permissions(),
|
||||
originating_turn_state
|
||||
.lock()
|
||||
.await
|
||||
.granted_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
Some(requested_permissions.into())
|
||||
);
|
||||
assert_eq!(current_turn_state.lock().await.granted_permissions(), None);
|
||||
assert_eq!(session.granted_turn_permissions().await, None);
|
||||
assert_eq!(
|
||||
current_turn_state
|
||||
.lock()
|
||||
.await
|
||||
.granted_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
session
|
||||
.granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_permission_grants_are_environment_keyed() {
|
||||
let (session, _turn_context) = make_session_and_context().await;
|
||||
let originating_active_turn = ActiveTurn::default();
|
||||
let originating_turn_state = Arc::clone(&originating_active_turn.turn_state);
|
||||
*session.active_turn.lock().await = Some(originating_active_turn);
|
||||
|
||||
let requested_permissions = RequestPermissionProfile {
|
||||
network: Some(codex_protocol::models::NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
..RequestPermissionProfile::default()
|
||||
};
|
||||
session
|
||||
.record_granted_request_permissions_for_turn(
|
||||
&codex_protocol::request_permissions::RequestPermissionsResponse {
|
||||
permissions: requested_permissions.clone(),
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
},
|
||||
"remote",
|
||||
Some(&originating_turn_state),
|
||||
)
|
||||
.await;
|
||||
|
||||
{
|
||||
let turn_state = originating_turn_state.lock().await;
|
||||
assert_eq!(
|
||||
turn_state.granted_permissions("remote"),
|
||||
Some(requested_permissions.clone().into())
|
||||
);
|
||||
assert_eq!(turn_state.granted_permissions("local"), None);
|
||||
}
|
||||
|
||||
session
|
||||
.record_granted_request_permissions_for_turn(
|
||||
&codex_protocol::request_permissions::RequestPermissionsResponse {
|
||||
permissions: requested_permissions.clone(),
|
||||
scope: PermissionGrantScope::Session,
|
||||
strict_auto_review: false,
|
||||
},
|
||||
"remote",
|
||||
/*originating_turn_state*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
session.granted_session_permissions("remote").await,
|
||||
Some(requested_permissions.into())
|
||||
);
|
||||
assert_eq!(session.granted_session_permissions("local").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -5214,6 +5288,7 @@ async fn enable_strict_auto_review_for_turn_uses_originating_turn() {
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: true,
|
||||
},
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID,
|
||||
Some(&originating_turn_state),
|
||||
)
|
||||
.await;
|
||||
@@ -5333,6 +5408,107 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
|
||||
assert_eq!(response, Some(expected_response));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_permissions_tool_resolves_relative_paths_against_primary_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 = {
|
||||
#[allow(deprecated)]
|
||||
let legacy_cwd = turn_context.cwd.clone();
|
||||
legacy_cwd.join("request-permissions-environment")
|
||||
};
|
||||
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");
|
||||
turn_context_mut
|
||||
.approval_policy
|
||||
.set(AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
rules: true,
|
||||
skill_approval: true,
|
||||
request_permissions: true,
|
||||
mcp_elicitations: true,
|
||||
}))
|
||||
.expect("test setup should allow updating approval policy");
|
||||
turn_context_mut.environments.turn_environments[0].cwd = environment_cwd.clone();
|
||||
|
||||
let call_id = "call-1".to_string();
|
||||
let handler = RequestPermissionsHandler;
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let handle = tokio::spawn({
|
||||
let session = Arc::clone(&session);
|
||||
let turn_context = Arc::clone(&turn_context);
|
||||
let tracker = Arc::clone(&tracker);
|
||||
let call_id = call_id.clone();
|
||||
async move {
|
||||
handler
|
||||
.handle(ToolInvocation {
|
||||
session,
|
||||
turn: turn_context,
|
||||
cancellation_token: CancellationToken::new(),
|
||||
tracker,
|
||||
call_id,
|
||||
tool_name: codex_tools::ToolName::plain("request_permissions"),
|
||||
source: ToolCallSource::Direct,
|
||||
payload: ToolPayload::Function {
|
||||
arguments: json!({
|
||||
"reason": "need write",
|
||||
"permissions": {
|
||||
"file_system": {
|
||||
"entries": [{
|
||||
"path": {
|
||||
"type": "path",
|
||||
"path": "relative.txt",
|
||||
},
|
||||
"access": "write",
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
}
|
||||
});
|
||||
|
||||
let request_event = tokio::time::timeout(StdDuration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("request_permissions event timed out")
|
||||
.expect("request_permissions event missing");
|
||||
let EventMsg::RequestPermissions(request) = request_event.msg else {
|
||||
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"),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}],
|
||||
glob_scan_max_depth: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(request.permissions, expected_permissions);
|
||||
|
||||
session
|
||||
.notify_request_permissions_response(
|
||||
&request.call_id,
|
||||
codex_protocol::request_permissions::RequestPermissionsResponse {
|
||||
permissions: request.permissions,
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
tokio::time::timeout(StdDuration::from_secs(1), handle)
|
||||
.await
|
||||
.expect("request_permissions handler timed out")
|
||||
.expect("request_permissions handler join error")
|
||||
.expect("request_permissions handler should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_permissions_response_materializes_session_cwd_grants_before_recording() {
|
||||
let (session, mut turn_context, rx) = make_session_and_context_with_rx().await;
|
||||
@@ -5425,7 +5601,9 @@ async fn request_permissions_response_materializes_session_cwd_grants_before_rec
|
||||
|
||||
assert_eq!(response, Some(expected_response));
|
||||
assert_eq!(
|
||||
session.granted_session_permissions().await,
|
||||
session
|
||||
.granted_session_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
|
||||
.await,
|
||||
Some(expected_permissions.into())
|
||||
);
|
||||
}
|
||||
@@ -10450,7 +10628,12 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
);
|
||||
|
||||
pretty_assertions::assert_eq!(output, expected);
|
||||
pretty_assertions::assert_eq!(session.granted_turn_permissions().await, None);
|
||||
pretty_assertions::assert_eq!(
|
||||
session
|
||||
.granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
|
||||
// The rejection should not poison the non-escalated path for the same
|
||||
// command. Force DangerFullAccess so this check stays focused on approval
|
||||
|
||||
@@ -148,7 +148,9 @@ async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() {
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
session.granted_turn_permissions().await,
|
||||
session
|
||||
.granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
|
||||
.await,
|
||||
Some(requested_permissions.into())
|
||||
);
|
||||
|
||||
@@ -246,7 +248,12 @@ async fn request_permissions_guardian_review_stops_when_cancelled() {
|
||||
.expect("request_permissions should stop when cancelled")
|
||||
.expect("request_permissions task should not panic");
|
||||
assert_eq!(response, None);
|
||||
assert_eq!(session.granted_turn_permissions().await, None);
|
||||
assert_eq!(
|
||||
session
|
||||
.granted_turn_permissions(codex_exec_server::LOCAL_ENVIRONMENT_ID)
|
||||
.await,
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -380,6 +387,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
|
||||
scope: PermissionGrantScope::Turn,
|
||||
strict_auto_review: true,
|
||||
},
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID,
|
||||
Some(&originating_turn_state),
|
||||
)
|
||||
.await;
|
||||
@@ -564,12 +572,15 @@ async fn shell_command_allows_sticky_turn_permissions_without_inline_request_per
|
||||
let mut active_turn = session.active_turn.lock().await;
|
||||
let active_turn = active_turn.as_mut().expect("active turn");
|
||||
let mut turn_state = active_turn.turn_state.lock().await;
|
||||
turn_state.record_granted_permissions(PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
turn_state.record_granted_permissions(
|
||||
codex_exec_server::LOCAL_ENVIRONMENT_ID,
|
||||
PermissionProfile {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let session = Arc::new(session);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
@@ -37,7 +38,7 @@ pub(crate) struct SessionState {
|
||||
pub(crate) startup_prewarm: Option<SessionStartupPrewarmHandle>,
|
||||
pub(crate) active_connector_selection: HashSet<String>,
|
||||
pub(crate) pending_session_start_sources: VecDeque<codex_hooks::SessionStartSource>,
|
||||
granted_permissions: Option<AdditionalPermissionProfile>,
|
||||
granted_permissions_by_environment_id: HashMap<String, AdditionalPermissionProfile>,
|
||||
next_turn_is_first: bool,
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ impl SessionState {
|
||||
startup_prewarm: None,
|
||||
active_connector_selection: HashSet::new(),
|
||||
pending_session_start_sources: VecDeque::new(),
|
||||
granted_permissions: None,
|
||||
granted_permissions_by_environment_id: HashMap::new(),
|
||||
next_turn_is_first: true,
|
||||
}
|
||||
}
|
||||
@@ -235,13 +236,29 @@ impl SessionState {
|
||||
self.pending_session_start_sources.pop_front()
|
||||
}
|
||||
|
||||
pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) {
|
||||
self.granted_permissions =
|
||||
merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions));
|
||||
pub(crate) fn record_granted_permissions(
|
||||
&mut self,
|
||||
environment_id: &str,
|
||||
permissions: AdditionalPermissionProfile,
|
||||
) {
|
||||
let granted_permissions = merge_permission_profiles(
|
||||
self.granted_permissions_by_environment_id
|
||||
.get(environment_id),
|
||||
Some(&permissions),
|
||||
);
|
||||
if let Some(granted_permissions) = granted_permissions {
|
||||
self.granted_permissions_by_environment_id
|
||||
.insert(environment_id.to_string(), granted_permissions);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn granted_permissions(&self) -> Option<AdditionalPermissionProfile> {
|
||||
self.granted_permissions.clone()
|
||||
pub(crate) fn granted_permissions(
|
||||
&self,
|
||||
environment_id: &str,
|
||||
) -> Option<AdditionalPermissionProfile> {
|
||||
self.granted_permissions_by_environment_id
|
||||
.get(environment_id)
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Turn-scoped state and active turn metadata scaffolding.
|
||||
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -10,11 +9,12 @@ use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use codex_extension_api::ExtensionData;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use rmcp::model::RequestId;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(crate) struct TurnState {
|
||||
pending_dynamic_tools: HashMap<String, oneshot::Sender<DynamicToolResponse>>,
|
||||
pub(crate) pending_input: TurnInputQueue,
|
||||
mailbox_delivery_phase: MailboxDeliveryPhase,
|
||||
granted_permissions: Option<AdditionalPermissionProfile>,
|
||||
granted_permissions_by_environment_id: HashMap<String, AdditionalPermissionProfile>,
|
||||
strict_auto_review_enabled: bool,
|
||||
pub(crate) tool_calls: u64,
|
||||
pub(crate) has_memory_citation: bool,
|
||||
@@ -100,7 +100,7 @@ pub(crate) struct TurnState {
|
||||
pub(crate) struct PendingRequestPermissions {
|
||||
pub(crate) tx_response: oneshot::Sender<RequestPermissionsResponse>,
|
||||
pub(crate) requested_permissions: RequestPermissionProfile,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) environment: TurnEnvironmentSelection,
|
||||
}
|
||||
|
||||
impl TurnState {
|
||||
@@ -204,13 +204,29 @@ impl TurnState {
|
||||
self.mailbox_delivery_phase = phase;
|
||||
}
|
||||
|
||||
pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) {
|
||||
self.granted_permissions =
|
||||
merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions));
|
||||
pub(crate) fn record_granted_permissions(
|
||||
&mut self,
|
||||
environment_id: &str,
|
||||
permissions: AdditionalPermissionProfile,
|
||||
) {
|
||||
let granted_permissions = merge_permission_profiles(
|
||||
self.granted_permissions_by_environment_id
|
||||
.get(environment_id),
|
||||
Some(&permissions),
|
||||
);
|
||||
if let Some(granted_permissions) = granted_permissions {
|
||||
self.granted_permissions_by_environment_id
|
||||
.insert(environment_id.to_string(), granted_permissions);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn granted_permissions(&self) -> Option<AdditionalPermissionProfile> {
|
||||
self.granted_permissions.clone()
|
||||
pub(crate) fn granted_permissions(
|
||||
&self,
|
||||
environment_id: &str,
|
||||
) -> Option<AdditionalPermissionProfile> {
|
||||
self.granted_permissions_by_environment_id
|
||||
.get(environment_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn enable_strict_auto_review(&mut self) {
|
||||
|
||||
@@ -265,6 +265,7 @@ fn apply_patch_payload_command(payload: &ToolPayload) -> Option<String> {
|
||||
async fn effective_patch_permissions(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
environment_id: &str,
|
||||
action: &ApplyPatchAction,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> (
|
||||
@@ -274,8 +275,14 @@ async fn effective_patch_permissions(
|
||||
) {
|
||||
let file_paths = file_paths_for_action(action);
|
||||
let granted_permissions = merge_permission_profiles(
|
||||
session.granted_session_permissions().await.as_ref(),
|
||||
session.granted_turn_permissions().await.as_ref(),
|
||||
session
|
||||
.granted_session_permissions(environment_id)
|
||||
.await
|
||||
.as_ref(),
|
||||
session
|
||||
.granted_turn_permissions(environment_id)
|
||||
.await
|
||||
.as_ref(),
|
||||
);
|
||||
let base_file_system_sandbox_policy = turn.file_system_sandbox_policy();
|
||||
let file_system_sandbox_policy = effective_file_system_sandbox_policy(
|
||||
@@ -284,6 +291,7 @@ async fn effective_patch_permissions(
|
||||
);
|
||||
let effective_additional_permissions = apply_granted_turn_permissions(
|
||||
session,
|
||||
environment_id,
|
||||
cwd.as_path(),
|
||||
crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
write_permissions_for_paths(&file_paths, &file_system_sandbox_policy, cwd),
|
||||
@@ -353,8 +361,14 @@ impl ToolExecutor<ToolInvocation> for ApplyPatchHandler {
|
||||
{
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let (file_paths, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, &cwd)
|
||||
.await;
|
||||
effective_patch_permissions(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
&changes,
|
||||
&cwd,
|
||||
)
|
||||
.await;
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
.await
|
||||
{
|
||||
@@ -506,7 +520,14 @@ pub(crate) async fn intercept_apply_patch(
|
||||
{
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let (approval_keys, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes, cwd).await;
|
||||
effective_patch_permissions(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
&changes,
|
||||
cwd,
|
||||
)
|
||||
.await;
|
||||
match apply_patch::apply_patch(turn.as_ref(), &file_system_sandbox_policy, changes)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -250,7 +250,8 @@ pub(super) fn implicit_granted_permissions(
|
||||
|
||||
pub(super) async fn apply_granted_turn_permissions(
|
||||
session: &Session,
|
||||
cwd: &std::path::Path,
|
||||
environment_id: &str,
|
||||
cwd: &Path,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
) -> EffectiveAdditionalPermissions {
|
||||
@@ -262,8 +263,8 @@ pub(super) async fn apply_granted_turn_permissions(
|
||||
};
|
||||
}
|
||||
|
||||
let granted_session_permissions = session.granted_session_permissions().await;
|
||||
let granted_turn_permissions = session.granted_turn_permissions().await;
|
||||
let granted_session_permissions = session.granted_session_permissions(environment_id).await;
|
||||
let granted_turn_permissions = session.granted_turn_permissions(environment_id).await;
|
||||
let granted_permissions = merge_permission_profiles(
|
||||
granted_session_permissions.as_ref(),
|
||||
granted_turn_permissions.as_ref(),
|
||||
|
||||
@@ -48,9 +48,13 @@ impl ToolExecutor<ToolInvocation> for RequestPermissionsHandler {
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(deprecated)]
|
||||
let Some(turn_environment) = turn.environments.primary() else {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"request_permissions requires a primary environment".to_string(),
|
||||
));
|
||||
};
|
||||
let mut args: RequestPermissionsArgs =
|
||||
parse_arguments_with_base_path(&arguments, &turn.cwd)?;
|
||||
parse_arguments_with_base_path(&arguments, &turn_environment.cwd)?;
|
||||
args.permissions = normalize_additional_permissions(args.permissions.into())
|
||||
.map(codex_protocol::request_permissions::RequestPermissionProfile::from)
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
|
||||
@@ -84,10 +84,10 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
let exec_permission_approvals_enabled =
|
||||
session.features().enabled(Feature::ExecPermissionApprovals);
|
||||
let requested_additional_permissions = additional_permissions.clone();
|
||||
#[allow(deprecated)]
|
||||
let effective_additional_permissions = apply_granted_turn_permissions(
|
||||
session.as_ref(),
|
||||
turn.cwd.as_path(),
|
||||
&turn_environment.environment_id,
|
||||
exec_params.cwd.as_path(),
|
||||
exec_params.sandbox_permissions,
|
||||
additional_permissions,
|
||||
)
|
||||
|
||||
@@ -173,6 +173,7 @@ impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
|
||||
let requested_additional_permissions = additional_permissions.clone();
|
||||
let effective_additional_permissions = apply_granted_turn_permissions(
|
||||
context.session.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
cwd.as_path(),
|
||||
sandbox_permissions,
|
||||
additional_permissions,
|
||||
|
||||
Reference in New Issue
Block a user