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:
jif
2026-06-02 20:16:57 +02:00
committed by GitHub
parent 9e3d5f29e2
commit 503ec190a8
10 changed files with 356 additions and 57 deletions
+24 -7
View File
@@ -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()
}
}
+25 -9
View File
@@ -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) {