Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions codex-rs/core/src/context_manager/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,66 @@ pub(crate) fn build_settings_update_items(
}
items
}

pub(crate) fn build_runtime_workspace_update_items(
previous: Option<&TurnContextItem>,
next: &TurnContextItem,
turn_context: &TurnContext,
shell: &Shell,
exec_policy: &Policy,
) -> Vec<ResponseItem> {
let contextual_user_message = if turn_context.config.include_environment_context {
let next_context =
EnvironmentContext::from_turn_context_item(next, shell.name().to_string());
match previous {
Some(previous) => {
let previous_context =
EnvironmentContext::from_turn_context_item(previous, shell.name().to_string());
(!previous_context.equals_except_shell(&next_context)).then(|| {
ContextualUserFragment::into(EnvironmentContext::diff_from_turn_context_item(
previous,
&next_context,
))
})
}
None => Some(ContextualUserFragment::into(next_context)),
}
} else {
None
};

let permissions_changed = previous.is_none_or(|previous| {
previous.permission_profile() != next.permission_profile()
|| previous.approval_policy != next.approval_policy
});
let developer_update_sections =
if turn_context.config.include_permissions_instructions && permissions_changed {
vec![
PermissionsInstructions::from_permission_profile(
&next.permission_profile(),
next.approval_policy,
turn_context.config.approvals_reviewer,
exec_policy,
&next.cwd,
turn_context
.features
.enabled(Feature::ExecPermissionApprovals),
turn_context
.features
.enabled(Feature::RequestPermissionsTool),
)
.render(),
]
} else {
Vec::new()
};

let mut items = Vec::with_capacity(/*capacity*/ 2);
if let Some(developer_message) = build_developer_update_item(developer_update_sections) {
items.push(developer_message);
}
if let Some(contextual_user_message) = contextual_user_message {
items.push(contextual_user_message);
}
items
}
5 changes: 5 additions & 0 deletions codex-rs/core/src/guardian/approval_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use codex_protocol::approvals::GuardianAssessmentAction;
use codex_protocol::approvals::GuardianCommandSource;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Serialize;
Expand Down Expand Up @@ -73,6 +74,7 @@ pub(crate) enum GuardianApprovalRequest {
turn_id: String,
reason: Option<String>,
permissions: RequestPermissionProfile,
requested_scope: PermissionGrantScope,
},
}

Expand Down Expand Up @@ -167,6 +169,7 @@ struct RequestPermissionsApprovalAction<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<&'a String>,
permissions: &'a RequestPermissionProfile,
requested_scope: PermissionGrantScope,
}

fn serialize_guardian_action(value: impl Serialize) -> serde_json::Result<Value> {
Expand Down Expand Up @@ -363,11 +366,13 @@ pub(crate) fn guardian_approval_request_to_json(
turn_id,
reason,
permissions,
requested_scope,
} => serialize_guardian_action(RequestPermissionsApprovalAction {
tool: "request_permissions",
turn_id,
reason: reason.as_ref(),
permissions,
requested_scope: *requested_scope,
}),
}
}
Expand Down
48 changes: 29 additions & 19 deletions codex-rs/core/src/guardian/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,26 +642,36 @@ pub(crate) fn spawn_approval_request_review(
approval_request_source: GuardianApprovalRequestSource,
cancel_token: CancellationToken,
) -> oneshot::Receiver<ReviewDecision> {
// Keep this above the default macOS pthread stack because permission-review
// sessions can recurse through thread materialization and state loading.
const GUARDIAN_REVIEW_THREAD_STACK_SIZE_BYTES: usize = 4 * 1024 * 1024;

let (tx, rx) = oneshot::channel();
std::thread::spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
let _ = tx.send(ReviewDecision::Denied);
return;
};
let decision = runtime.block_on(review_approval_request_with_cancel(
&session,
&turn,
review_id,
request,
retry_reason,
approval_request_source,
cancel_token,
));
let _ = tx.send(decision);
});
// Guardian reviews materialize a nested Codex session and can overflow the
// default macOS pthread stack when reviewing permission requests.
// Dropping the sender if spawning fails closes the channel, which callers treat as denial.
let _ = std::thread::Builder::new()
.name("guardian-approval-request-review".to_string())
.stack_size(GUARDIAN_REVIEW_THREAD_STACK_SIZE_BYTES)
.spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
let _ = tx.send(ReviewDecision::Denied);
return;
};
let decision = runtime.block_on(review_approval_request_with_cancel(
&session,
&turn,
review_id,
request,
retry_reason,
approval_request_source,
cancel_token,
));
let _ = tx.send(decision);
});
rx
}

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/session/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ async fn thread_settings_update(
}
}

async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
pub(crate) async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
let snapshot = {
let state = sess.state.lock().await;
state.session_configuration.thread_config_snapshot()
Expand Down
137 changes: 134 additions & 3 deletions codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,62 @@ impl Session {
Ok(())
}

pub(crate) async fn runtime_workspace_snapshot(&self) -> session::RuntimeWorkspaceSnapshot {
let state = self.state.lock().await;
session::RuntimeWorkspaceSnapshot {
cwd: state.session_configuration.cwd().clone(),
workspace_roots: state.session_configuration.workspace_roots.clone(),
permission_profile: state.session_configuration.permission_profile(),
}
}

pub(crate) async fn update_runtime_cwd(
&self,
turn_context: &TurnContext,
cwd: AbsolutePathBuf,
) -> ConstraintResult<()> {
let environments = {
let state = self.state.lock().await;
let mut environments = state.session_configuration.environments.clone();
environments.legacy_fallback_cwd = cwd.clone();
if let Some(environment) = environments.environments.first_mut() {
environment.cwd = PathUri::from_abs_path(&cwd);
}
environments
};
self.update_settings(SessionSettingsUpdate {
environments: Some(environments),
..Default::default()
})
.await?;

let runtime_workspace = self.runtime_workspace_snapshot().await;
let turn_context_item =
turn_context.to_turn_context_item_with_runtime_workspace(&runtime_workspace);
let reference_context_item = self.reference_context_item().await;
let shell = self.user_shell();
let exec_policy = self.services.exec_policy.current();
let context_items = crate::context_manager::updates::build_runtime_workspace_update_items(
reference_context_item.as_ref(),
&turn_context_item,
turn_context,
shell.as_ref(),
exec_policy.as_ref(),
);
self.record_context_items_and_set_reference_context_item(
turn_context,
context_items,
turn_context_item,
)
.await;
self.send_event(
turn_context,
handlers::thread_settings_applied_event(self).await,
)
.await;
Ok(())
}

pub(crate) async fn preview_settings(
&self,
updates: &SessionSettingsUpdate,
Expand Down Expand Up @@ -2168,16 +2224,36 @@ impl Session {
rx_approve
}

pub(crate) async fn request_permissions_for_environment(
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
call_id: String,
args: RequestPermissionsArgs,
environment: TurnEnvironmentSelection,
cancellation_token: CancellationToken,
) -> Option<RequestPermissionsResponse> {
self.request_permissions_for_environment_with_scope(
turn_context,
call_id,
args,
environment,
/*requested_scope*/ PermissionGrantScope::Turn,
cancellation_token,
)
.await
}

#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and turn state updates must remain atomic"
)]
pub(crate) async fn request_permissions_for_environment(
async fn request_permissions_for_environment_with_scope(
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
call_id: String,
args: RequestPermissionsArgs,
environment: TurnEnvironmentSelection,
requested_scope: PermissionGrantScope,
cancellation_token: CancellationToken,
) -> Option<RequestPermissionsResponse> {
match turn_context.as_ref().approval_policy.value() {
Expand Down Expand Up @@ -2230,6 +2306,7 @@ impl Session {
turn_id: turn_context.sub_id.clone(),
reason: args.reason,
permissions: requested_permissions.clone(),
requested_scope,
};
let review_rx = crate::guardian::spawn_approval_request_review(
session,
Expand All @@ -2249,7 +2326,7 @@ impl Session {
ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
RequestPermissionsResponse {
permissions: requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
scope: requested_scope,
strict_auto_review: false,
}
}
Expand Down Expand Up @@ -2347,6 +2424,45 @@ impl Session {
args: RequestPermissionsArgs,
cwd: AbsolutePathBuf,
cancellation_token: CancellationToken,
) -> Option<RequestPermissionsResponse> {
self.request_permissions_for_cwd_with_scope(
turn_context,
call_id,
args,
cwd,
/*requested_scope*/ PermissionGrantScope::Turn,
cancellation_token,
)
.await
}

pub(crate) async fn request_session_permissions_for_cwd(
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
call_id: String,
args: RequestPermissionsArgs,
cwd: AbsolutePathBuf,
cancellation_token: CancellationToken,
) -> Option<RequestPermissionsResponse> {
self.request_permissions_for_cwd_with_scope(
turn_context,
call_id,
args,
cwd,
/*requested_scope*/ PermissionGrantScope::Session,
cancellation_token,
)
.await
}

async fn request_permissions_for_cwd_with_scope(
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
call_id: String,
args: RequestPermissionsArgs,
cwd: AbsolutePathBuf,
requested_scope: PermissionGrantScope,
cancellation_token: CancellationToken,
) -> Option<RequestPermissionsResponse> {
let turn_environment = match args.environment_id.as_deref() {
Some(environment_id) => turn_context
Expand All @@ -2365,11 +2481,12 @@ impl Session {
};
let mut environment = turn_environment.selection();
environment.cwd = PathUri::from_abs_path(&cwd);
self.request_permissions_for_environment(
self.request_permissions_for_environment_with_scope(
turn_context,
call_id,
args,
environment,
requested_scope,
cancellation_token,
)
.await
Expand Down Expand Up @@ -3200,6 +3317,20 @@ impl Session {
.await
};
let turn_context_item = turn_context.to_turn_context_item();
self.record_context_items_and_set_reference_context_item(
turn_context,
context_items,
turn_context_item,
)
.await;
}

async fn record_context_items_and_set_reference_context_item(
&self,
turn_context: &TurnContext,
context_items: Vec<ResponseItem>,
turn_context_item: TurnContextItem,
) {
if !context_items.is_empty() {
self.record_conversation_items(turn_context, &context_items)
.await;
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,13 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) app_server_client_version: Option<String>,
}

#[derive(Clone, Debug)]
pub(crate) struct RuntimeWorkspaceSnapshot {
pub(crate) cwd: AbsolutePathBuf,
pub(crate) workspace_roots: Vec<AbsolutePathBuf>,
pub(crate) permission_profile: PermissionProfile,
}

pub(crate) struct AppServerClientMetadata {
pub(crate) client_name: Option<String>,
pub(crate) client_version: Option<String>,
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/core/src/session/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1985,7 +1985,13 @@ async fn try_run_sampling_request(
Err(err) => break Err(err),
};
if let Some(tool_future) = output_result.tool_future {
in_flight.push_back(tool_future);
if output_result.tool_is_execution_barrier {
drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?;
in_flight.push_back(tool_future);
drain_in_flight(&mut in_flight, sess.clone(), turn_context.clone()).await?;
} else {
in_flight.push_back(tool_future);
}
}
if let Some(agent_message) = output_result.last_agent_message {
last_agent_message = Some(agent_message);
Expand Down
Loading
Loading