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:
Adam Perry @ OpenAI
2026-06-15 19:25:54 -07:00
committed by GitHub
Unverified
parent d959664420
commit ecfe174d5f
34 changed files with 546 additions and 233 deletions
@@ -30,7 +30,16 @@ impl ChatWidget {
self.on_elicitation_request(request_id, params);
}
ServerRequest::PermissionsRequestApproval { params, .. } => {
self.on_request_permissions(request_permissions_from_params(params));
// TODO(anp): Remove this native-path localization error path once core permission
// paths remain PathUri after crossing the app-server boundary.
match request_permissions_from_params(params) {
Ok(event) => self.on_request_permissions(event),
Err(err) => {
self.add_error_message(format!(
"failed to localize requested filesystem paths: {err}"
));
}
}
}
ServerRequest::ToolRequestUserInput { params, .. } => {
self.on_request_user_input(params);
@@ -62,6 +71,17 @@ impl ChatWidget {
completion: Option<(i64, codex_app_server_protocol::AutoReviewDecisionSource)>,
action: GuardianApprovalReviewAction,
) {
// TODO(anp): Remove this native-path localization error path once core permission paths
// remain PathUri after crossing the app-server boundary.
let action = match action.try_into() {
Ok(action) => action,
Err(err) => {
self.add_error_message(format!(
"failed to localize guardian filesystem paths: {err}"
));
return;
}
};
let (completed_at_ms, decision_source) = match completion {
Some((completed_at_ms, decision_source)) => {
(Some(completed_at_ms), Some(decision_source))
@@ -128,7 +148,7 @@ impl ChatWidget {
GuardianAssessmentDecisionSource::Agent
}
}),
action: action.into(),
action,
});
}
+1
View File
@@ -164,6 +164,7 @@ pub(super) use codex_terminal_detection::TerminalInfo;
pub(super) use codex_terminal_detection::TerminalName;
pub(super) use codex_utils_absolute_path::AbsolutePathBuf;
pub(super) use codex_utils_approval_presets::builtin_approval_presets;
pub(super) use codex_utils_path_uri::ApiPathString;
pub(super) use crossterm::event::KeyCode;
pub(super) use crossterm::event::KeyEvent;
pub(super) use crossterm::event::KeyModifiers;
@@ -89,6 +89,8 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
.expect("absolute read path");
let write_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/write")))
.expect("absolute write path");
let read_api_path = ApiPathString::from_abs_path(&read_path);
let write_api_path = ApiPathString::from_abs_path(&write_path);
let request = exec_approval_request_from_params(
AppServerCommandExecutionRequestApprovalParams {
thread_id: "thread-1".to_string(),
@@ -109,8 +111,8 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
enabled: Some(true),
}),
file_system: Some(AppServerAdditionalFileSystemPermissions {
read: Some(vec![read_path.clone()]),
write: Some(vec![write_path.clone()]),
read: Some(vec![read_api_path.clone()]),
write: Some(vec![write_api_path.clone()]),
glob_scan_max_depth: None,
entries: None,
}),
@@ -136,8 +138,8 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
enabled: Some(true),
}),
file_system: Some(AppServerAdditionalFileSystemPermissions {
read: Some(vec![read_path]),
write: Some(vec![write_path]),
read: Some(vec![read_api_path]),
write: Some(vec![write_api_path]),
glob_scan_max_depth: None,
entries: None,
}),
@@ -274,6 +276,8 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
.expect("absolute read path");
let write_path = AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp/write")))
.expect("absolute write path");
let read_api_path = ApiPathString::from_abs_path(&read_path);
let write_api_path = ApiPathString::from_abs_path(&write_path);
let cwd =
AbsolutePathBuf::try_from(PathBuf::from(test_path_display("/tmp"))).expect("absolute cwd");
@@ -290,13 +294,14 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
enabled: Some(true),
}),
file_system: Some(AppServerAdditionalFileSystemPermissions {
read: Some(vec![read_path.clone()]),
write: Some(vec![write_path.clone()]),
read: Some(vec![read_api_path]),
write: Some(vec![write_api_path]),
glob_scan_max_depth: None,
entries: None,
}),
},
});
})
.expect("API paths should convert to native paths");
assert_eq!(
request.permissions,