mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
app-server: accept command permission profiles (#18283)
## Why `command/exec` is another app-server entry point that can run under caller-provided permissions. It needs to accept `PermissionProfile` directly so command execution is not left behind on `SandboxPolicy` while thread APIs move forward. Command-level profiles also need to preserve the semantics clients expect from profile-relative paths. `:cwd` and cwd-relative deny globs should be anchored to the resolved command cwd for a command-specific profile, while configured deny-read restrictions such as `**/*.env = none` still need to be enforced because they can come from config or requirements rather than the command override itself. ## What Changed This adds `permissionProfile` to `CommandExecParams`, rejects requests that combine it with `sandboxPolicy`, and converts accepted profiles into the runtime filesystem/network permissions used for command execution. When a command supplies a profile, the app-server resolves that profile against the command cwd instead of the thread/server cwd. It also preserves configured deny-read entries and `globScanMaxDepth` on the effective filesystem policy so one-off command overrides cannot drop those read protections. The PR also updates app-server docs/schema fixtures and adds command-exec coverage for accepted, rejected, cwd-scoped, and deny-read-preserving profile paths. ## Verification - `cargo test -p codex-app-server command_exec_permission_profile_cwd_uses_command_cwd` - `cargo test -p codex-app-server command_profile_preserves_configured_deny_read_restrictions` - `cargo test -p codex-app-server command_exec_accepts_permission_profile` - `cargo test -p codex-app-server command_exec_rejects_sandbox_policy_with_permission_profile` - `just fix -p codex-app-server` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18283). * #18288 * #18287 * #18286 * #18285 * #18284 * __->__ #18283
This commit is contained in:
committed by
GitHub
Unverified
parent
bbff4ee61a
commit
9d824cf4b4
@@ -310,6 +310,8 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
@@ -2073,7 +2075,16 @@ impl CodexMessageProcessor {
|
||||
env: env_overrides,
|
||||
size,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
} = params;
|
||||
if sandbox_policy.is_some() && permission_profile.is_some() {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"`permissionProfile` cannot be combined with `sandboxPolicy`".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
if size.is_some() && !tty {
|
||||
let error = JSONRPCErrorError {
|
||||
@@ -2185,7 +2196,11 @@ impl CodexMessageProcessor {
|
||||
} else {
|
||||
ExecCapturePolicy::ShellTool
|
||||
};
|
||||
let sandbox_cwd = self.config.cwd.clone();
|
||||
let sandbox_cwd = if permission_profile.is_some() {
|
||||
cwd.clone()
|
||||
} else {
|
||||
self.config.cwd.clone()
|
||||
};
|
||||
let exec_params = ExecParams {
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
@@ -2205,13 +2220,56 @@ impl CodexMessageProcessor {
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let requested_policy = sandbox_policy.map(|policy| policy.to_core());
|
||||
let (
|
||||
effective_policy,
|
||||
effective_file_system_sandbox_policy,
|
||||
effective_network_sandbox_policy,
|
||||
) = match requested_policy {
|
||||
Some(policy) => match self.config.permissions.sandbox_policy.can_set(&policy) {
|
||||
) = if let Some(permission_profile) = permission_profile {
|
||||
let permission_profile =
|
||||
codex_protocol::models::PermissionProfile::from(permission_profile);
|
||||
let sandbox_policy = match permission_profile.to_legacy_sandbox_policy(&sandbox_cwd) {
|
||||
Ok(sandbox_policy) => sandbox_policy,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid permission profile: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
match self
|
||||
.config
|
||||
.permissions
|
||||
.sandbox_policy
|
||||
.can_set(&sandbox_policy)
|
||||
{
|
||||
Ok(()) => {
|
||||
let (mut file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
Self::preserve_configured_deny_read_restrictions(
|
||||
&mut file_system_sandbox_policy,
|
||||
&self.config.permissions.file_system_sandbox_policy,
|
||||
);
|
||||
(
|
||||
sandbox_policy,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("invalid permission profile: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) {
|
||||
match self.config.permissions.sandbox_policy.can_set(&policy) {
|
||||
Ok(()) => {
|
||||
let file_system_sandbox_policy =
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy, &sandbox_cwd);
|
||||
@@ -2228,12 +2286,13 @@ impl CodexMessageProcessor {
|
||||
self.outgoing.send_error(request, error).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => (
|
||||
}
|
||||
} else {
|
||||
(
|
||||
self.config.permissions.sandbox_policy.get().clone(),
|
||||
self.config.permissions.file_system_sandbox_policy.clone(),
|
||||
self.config.permissions.network_sandbox_policy,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
|
||||
@@ -2290,6 +2349,30 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn preserve_configured_deny_read_restrictions(
|
||||
file_system_sandbox_policy: &mut FileSystemSandboxPolicy,
|
||||
configured_file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
) {
|
||||
if file_system_sandbox_policy.glob_scan_max_depth.is_none() {
|
||||
file_system_sandbox_policy.glob_scan_max_depth =
|
||||
configured_file_system_sandbox_policy.glob_scan_max_depth;
|
||||
}
|
||||
|
||||
for deny_entry in configured_file_system_sandbox_policy
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access == FileSystemAccessMode::None)
|
||||
{
|
||||
if !file_system_sandbox_policy
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry == deny_entry)
|
||||
{
|
||||
file_system_sandbox_policy.entries.push(deny_entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn command_exec_write(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -10085,6 +10168,8 @@ mod tests {
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
@@ -10352,6 +10437,36 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_profile_preserves_configured_deny_read_restrictions() {
|
||||
let readable_entry = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: test_path_buf("/tmp/project").abs(),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
};
|
||||
let deny_entry = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: "/tmp/project/**/*.env".to_string(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
};
|
||||
let mut file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![readable_entry.clone()]);
|
||||
let mut configured_file_system_sandbox_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![deny_entry.clone()]);
|
||||
configured_file_system_sandbox_policy.glob_scan_max_depth = Some(2);
|
||||
|
||||
CodexMessageProcessor::preserve_configured_deny_read_restrictions(
|
||||
&mut file_system_sandbox_policy,
|
||||
&configured_file_system_sandbox_policy,
|
||||
);
|
||||
|
||||
let mut expected = FileSystemSandboxPolicy::restricted(vec![readable_entry, deny_entry]);
|
||||
expected.glob_scan_max_depth = Some(2);
|
||||
assert_eq!(file_system_sandbox_policy, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_load_error_marks_cloud_requirements_failures_for_relogin() {
|
||||
let err = std::io::Error::other(CloudRequirementsLoadError::new(
|
||||
|
||||
Reference in New Issue
Block a user