mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Use ApiPathString in app-server filesystem permission paths (#28367)
## Why Clients running an app-server on one OS and an exec-server on another OS need to be able to pass sandbox config to app-server that refers to resources on the executor's foreign OS. ## What `AbsolutePathBuf` can't represent these paths and we don't want users to be exposed to `PathUri` yet, so this moves the public app-server API to be expressed in terms of `ApiPathString`. Stacked on #28165. - change app-server v2 filesystem permission paths, including legacy read/write roots, to `ApiPathString` - localize API paths through `PathUri` when converting into the current native core permission types - make path-bearing permission conversions fallible and surface localization failures instead of silently treating malformed grants as ordinary denials - propagate conversion failures through app-server and TUI approval handling - regenerate the app-server JSON and TypeScript schemas - leave migration TODOs on native-path conversions so they can be removed once core permission paths use `PathUri`
This commit is contained in:
committed by
GitHub
Unverified
parent
d959664420
commit
ecfe174d5f
@@ -786,6 +786,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
let pending_response = PendingRequestPermissionsResponse {
|
||||
call_id: request.call_id,
|
||||
conversation_id,
|
||||
turn_id: request.turn_id,
|
||||
requested_permissions,
|
||||
request_cwd,
|
||||
pending_request_id,
|
||||
@@ -948,15 +950,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
codex_error_info: ev.codex_error_info.map(V2CodexErrorInfo::from),
|
||||
additional_details: None,
|
||||
};
|
||||
handle_error(conversation_id, turn_error.clone(), &thread_state).await;
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::Error(ErrorNotification {
|
||||
error: turn_error.clone(),
|
||||
will_retry: false,
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
}))
|
||||
.await;
|
||||
handle_error_notification(
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
turn_error,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
EventMsg::StreamError(ev) => {
|
||||
// We don't need to update the turn summary store for stream errors as they are intermediate error states for retries,
|
||||
@@ -1641,6 +1642,24 @@ async fn handle_error(
|
||||
state.turn_summary.last_error = Some(error);
|
||||
}
|
||||
|
||||
async fn handle_error_notification(
|
||||
conversation_id: ThreadId,
|
||||
event_turn_id: &str,
|
||||
error: TurnError,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
) {
|
||||
handle_error(conversation_id, error.clone(), thread_state).await;
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::Error(ErrorNotification {
|
||||
error,
|
||||
will_retry: false,
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.to_string(),
|
||||
}))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn on_request_user_input_response(
|
||||
event_turn_id: String,
|
||||
pending_request_id: RequestId,
|
||||
@@ -1796,6 +1815,8 @@ async fn on_request_permissions_response(
|
||||
) {
|
||||
let PendingRequestPermissionsResponse {
|
||||
call_id,
|
||||
conversation_id,
|
||||
turn_id,
|
||||
requested_permissions,
|
||||
request_cwd,
|
||||
pending_request_id,
|
||||
@@ -1806,12 +1827,34 @@ async fn on_request_permissions_response(
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id.clone()).await;
|
||||
drop(request_permissions_guard);
|
||||
let Some(response) = request_permissions_response_from_client_result(
|
||||
let response = match request_permissions_response_from_client_result(
|
||||
requested_permissions,
|
||||
response,
|
||||
request_cwd.as_path(),
|
||||
) else {
|
||||
return;
|
||||
) {
|
||||
Ok(Some(response)) => response,
|
||||
Ok(None) => return,
|
||||
// TODO(anp): Remove this native-path localization error path once core permission paths
|
||||
// remain PathUri after crossing the app-server boundary.
|
||||
Err(err) => {
|
||||
let message = format!("failed to localize granted filesystem paths: {err}");
|
||||
handle_error_notification(
|
||||
conversation_id,
|
||||
&turn_id,
|
||||
TurnError {
|
||||
message,
|
||||
codex_error_info: None,
|
||||
additional_details: None,
|
||||
},
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = conversation.submit(Op::Interrupt).await {
|
||||
error!("failed to interrupt turn after invalid permission paths: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
outgoing.track_effective_permissions_approval_response(pending_request_id, response.clone());
|
||||
|
||||
@@ -1828,6 +1871,8 @@ async fn on_request_permissions_response(
|
||||
|
||||
struct PendingRequestPermissionsResponse {
|
||||
call_id: String,
|
||||
conversation_id: ThreadId,
|
||||
turn_id: String,
|
||||
requested_permissions: CoreRequestPermissionProfile,
|
||||
request_cwd: AbsolutePathBuf,
|
||||
pending_request_id: RequestId,
|
||||
@@ -1840,25 +1885,25 @@ fn request_permissions_response_from_client_result(
|
||||
requested_permissions: CoreRequestPermissionProfile,
|
||||
response: std::result::Result<ClientRequestResult, oneshot::error::RecvError>,
|
||||
cwd: &std::path::Path,
|
||||
) -> Option<CoreRequestPermissionsResponse> {
|
||||
) -> std::io::Result<Option<CoreRequestPermissionsResponse>> {
|
||||
let value = match response {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return None,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return Ok(None),
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
return Some(CoreRequestPermissionsResponse {
|
||||
return Ok(Some(CoreRequestPermissionsResponse {
|
||||
permissions: Default::default(),
|
||||
scope: CorePermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
}));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("request failed: {err:?}");
|
||||
return Some(CoreRequestPermissionsResponse {
|
||||
return Ok(Some(CoreRequestPermissionsResponse {
|
||||
permissions: Default::default(),
|
||||
scope: CorePermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1879,23 +1924,23 @@ fn request_permissions_response_from_client_result(
|
||||
)
|
||||
{
|
||||
error!("strict auto review is only supported for turn-scoped permission grants");
|
||||
return Some(CoreRequestPermissionsResponse {
|
||||
return Ok(Some(CoreRequestPermissionsResponse {
|
||||
permissions: Default::default(),
|
||||
scope: CorePermissionGrantScope::Turn,
|
||||
strict_auto_review: false,
|
||||
});
|
||||
}));
|
||||
}
|
||||
let granted_permissions: CoreAdditionalPermissionProfile = response.permissions.into();
|
||||
let granted_permissions: CoreAdditionalPermissionProfile = response.permissions.try_into()?;
|
||||
let permissions = if granted_permissions.is_empty() {
|
||||
CoreRequestPermissionProfile::default()
|
||||
} else {
|
||||
intersect_permission_profiles(requested_permissions.into(), granted_permissions, cwd).into()
|
||||
};
|
||||
Some(CoreRequestPermissionsResponse {
|
||||
Ok(Some(CoreRequestPermissionsResponse {
|
||||
permissions,
|
||||
scope: response.scope.to_core(),
|
||||
strict_auto_review,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
@@ -2899,7 +2944,8 @@ mod tests {
|
||||
CoreRequestPermissionProfile::default(),
|
||||
Ok(Err(error)),
|
||||
std::env::current_dir().expect("current dir").as_path(),
|
||||
);
|
||||
)
|
||||
.expect("paths should localize");
|
||||
|
||||
assert_eq!(response, None);
|
||||
}
|
||||
@@ -2994,6 +3040,7 @@ mod tests {
|
||||
}))),
|
||||
cwd.as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
@@ -3017,6 +3064,7 @@ mod tests {
|
||||
}))),
|
||||
std::env::current_dir().expect("current dir").as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
@@ -3044,6 +3092,7 @@ mod tests {
|
||||
}))),
|
||||
std::env::current_dir().expect("current dir").as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
@@ -3075,6 +3124,7 @@ mod tests {
|
||||
}))),
|
||||
std::env::current_dir().expect("current dir").as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(response.scope, CorePermissionGrantScope::Turn);
|
||||
@@ -3110,6 +3160,7 @@ mod tests {
|
||||
}))),
|
||||
cwd.as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
@@ -3156,6 +3207,7 @@ mod tests {
|
||||
}))),
|
||||
request_cwd.as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
@@ -3197,6 +3249,7 @@ mod tests {
|
||||
}))),
|
||||
cwd.as_path(),
|
||||
)
|
||||
.expect("paths should localize")
|
||||
.expect("response should be accepted");
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -260,7 +260,7 @@ async fn command_execution_request_approval_strips_additional_permissions_withou
|
||||
network: None,
|
||||
file_system: Some(
|
||||
codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
read: Some(vec![absolute_path("/tmp/allowed").into()]),
|
||||
write: None,
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
@@ -325,7 +325,7 @@ async fn command_execution_request_approval_keeps_additional_permissions_with_ca
|
||||
network: None,
|
||||
file_system: Some(
|
||||
codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
read: Some(vec![absolute_path("/tmp/allowed").into()]),
|
||||
write: None,
|
||||
glob_scan_max_depth: None,
|
||||
entries: None,
|
||||
|
||||
Reference in New Issue
Block a user