mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
unified-exec: preserve PathUri through exec-server (#28681)
## Why It should be possible for app-server to handle "foreign" OS paths in unified_exec working directories, allowing e.g. a Linux app-server to run processes on e.g. a Windows exec-server. ## What Convert the core unified_exec cwd values to use `PathUri`. Adds fallible path conversion in several places to try to minimize the scope of this change. The only time this change suppresses errors from converting `PathUri` to an `AbsolutePathBuf` is when the turn is configured with no sandboxing at all to allow us to make progress testing without sandboxing. Future changes to apply_patch and sandboxing will clean up these error paths. A tool's cwd is resolved from joining a model-provided workdir to the environment's cwd. When using `AbsolutePathBuf::join()`, an absolute-path workdir would overwrite the environment's cwd and we would resolve permissions/sandboxing against the model-provided path. This change extends `PathUri::join()` to also treat an absolute rhs as an override of the base/lhs. This also removes some coverage from the remove_env_windows tests until a follow-up converts foreign paths in command exec events correctly. ## Breaking Changes When using `AbsolutePathBuf::join()` for workdir resolution, we ended up resolving tilde-prefixed paths against the app-server's `$HOME`, e.g. `~/foo/bar` becomes `/home/anp/foo/bar`. It's difficult to do this with `PathUri` joining, so after offline discussion this PR no longer implements it. A quick check of some power users' rollouts suggests that models don't actually generate home-prefixed absolute working directories for their spawns, so this shouldn't have any real blast radius.
This commit is contained in:
committed by
GitHub
Unverified
parent
cca39d51ba
commit
5867b529ae
@@ -237,6 +237,10 @@ impl CommandExecManager {
|
||||
arg0,
|
||||
..
|
||||
} = exec_request;
|
||||
// TODO(anp): Keep PathUri through the local command launch boundary.
|
||||
let cwd = cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| invalid_request(format!("invalid command cwd: {err}")))?;
|
||||
|
||||
let stream_stdin = tty || stream_stdin;
|
||||
let stream_stdout_stderr = tty || stream_stdout_stderr;
|
||||
|
||||
@@ -1781,16 +1781,22 @@ impl ThreadRequestProcessor {
|
||||
.list_background_terminals()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|terminal| ThreadBackgroundTerminal {
|
||||
item_id: terminal.item_id,
|
||||
process_id: terminal.process_id,
|
||||
command: terminal.command,
|
||||
cwd: terminal.cwd,
|
||||
os_pid: None,
|
||||
cpu_percent: None,
|
||||
rss_kb: None,
|
||||
.map(|terminal| {
|
||||
// TODO(anp): Migrate ThreadBackgroundTerminal to PathUri.
|
||||
let cwd = terminal.cwd.to_abs_path().map_err(|err| {
|
||||
internal_error(format!("background terminal has invalid cwd: {err}"))
|
||||
})?;
|
||||
Ok(ThreadBackgroundTerminal {
|
||||
item_id: terminal.item_id,
|
||||
process_id: terminal.process_id,
|
||||
command: terminal.command,
|
||||
cwd,
|
||||
os_pid: None,
|
||||
cpu_percent: None,
|
||||
rss_kb: None,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Result<Vec<_>, JSONRPCErrorError>>()?;
|
||||
|
||||
let (data, next_cursor) = paginate_background_terminals(&terminals, cursor, limit)?;
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ pub async fn verify_apply_patch_args(
|
||||
MaybeApplyPatchVerified::Body(ApplyPatchAction {
|
||||
changes,
|
||||
patch,
|
||||
cwd: effective_cwd,
|
||||
cwd: effective_cwd.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -811,7 +811,9 @@ PATCH"#,
|
||||
},
|
||||
)]),
|
||||
patch: argv[1].clone(),
|
||||
cwd: AbsolutePathBuf::from_absolute_path(session_dir.path()).unwrap(),
|
||||
cwd: AbsolutePathBuf::from_absolute_path(session_dir.path())
|
||||
.unwrap()
|
||||
.into(),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -851,7 +853,10 @@ PATCH"#,
|
||||
other => panic!("expected verified body, got {other:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(action.cwd.as_path(), worktree_dir.as_path());
|
||||
assert_eq!(
|
||||
action.cwd.to_abs_path().unwrap().as_path(),
|
||||
worktree_dir.as_path()
|
||||
);
|
||||
|
||||
let source_path = worktree_dir.join(source_name);
|
||||
let change = action
|
||||
|
||||
@@ -142,7 +142,7 @@ pub struct ApplyPatchAction {
|
||||
pub patch: String,
|
||||
|
||||
/// The working directory that was used to resolve relative paths in the patch.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
}
|
||||
|
||||
impl ApplyPatchAction {
|
||||
@@ -174,7 +174,7 @@ impl ApplyPatchAction {
|
||||
#[expect(clippy::expect_used)]
|
||||
Self {
|
||||
changes,
|
||||
cwd: path.parent().expect("path should have parent"),
|
||||
cwd: path.parent().expect("path should have parent").into(),
|
||||
patch,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,24 @@ pub(crate) async fn apply_patch(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
action: ApplyPatchAction,
|
||||
) -> InternalApplyPatchInvocation {
|
||||
// TODO(anp): Migrate patch safety checks to PathUri.
|
||||
let cwd = match action.cwd.to_abs_path() {
|
||||
Ok(cwd) => cwd,
|
||||
Err(err) => {
|
||||
return InternalApplyPatchInvocation::Output(Err(FunctionCallError::RespondToModel(
|
||||
format!(
|
||||
"patch cwd `{}` is not native to the Codex host: {err}",
|
||||
action.cwd
|
||||
),
|
||||
)));
|
||||
}
|
||||
};
|
||||
match assess_patch_safety(
|
||||
&action,
|
||||
turn_context.approval_policy.value(),
|
||||
&turn_context.permission_profile(),
|
||||
file_system_sandbox_policy,
|
||||
&action.cwd,
|
||||
&cwd,
|
||||
turn_context.windows_sandbox_level,
|
||||
) {
|
||||
SafetyCheck::AutoApprove {
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_thread_store::ThreadMetadataPatch;
|
||||
use codex_thread_store::ThreadStoreError;
|
||||
use codex_thread_store::ThreadStoreResult;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use rmcp::model::ReadResourceRequestParams;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
@@ -164,7 +165,7 @@ pub struct BackgroundTerminalInfo {
|
||||
pub item_id: String,
|
||||
pub process_id: String,
|
||||
pub command: String,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
}
|
||||
|
||||
/// Conduit for the bidirectional stream of messages that compose a thread
|
||||
|
||||
@@ -381,7 +381,7 @@ pub fn build_exec_request(
|
||||
})
|
||||
.map(|request| {
|
||||
let windows_sandbox_workspace_roots = if windows_sandbox_workspace_roots.is_empty() {
|
||||
vec![request.sandbox_policy_cwd.clone()]
|
||||
vec![sandbox_cwd.clone()]
|
||||
} else {
|
||||
windows_sandbox_workspace_roots.to_vec()
|
||||
};
|
||||
@@ -440,6 +440,15 @@ pub(crate) async fn execute_exec_request(
|
||||
arg0,
|
||||
} = exec_request;
|
||||
|
||||
// TODO(anp): Keep PathUri through the local process launch boundary.
|
||||
let cwd = cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| CodexErr::InvalidRequest(format!("invalid exec cwd: {err}")))?;
|
||||
// TODO(anp): Keep PathUri through the Windows sandbox launch boundary.
|
||||
let windows_sandbox_policy_cwd = windows_sandbox_policy_cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| CodexErr::InvalidRequest(format!("invalid sandbox cwd: {err}")))?;
|
||||
|
||||
let params = ExecParams {
|
||||
command,
|
||||
cwd,
|
||||
|
||||
@@ -25,6 +25,7 @@ use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -42,14 +43,14 @@ pub(crate) struct ExecServerEnvConfig {
|
||||
#[derive(Debug)]
|
||||
pub struct ExecRequest {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub env: HashMap<String, String>,
|
||||
pub(crate) exec_server_env_config: Option<ExecServerEnvConfig>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
pub expiration: ExecExpiration,
|
||||
pub capture_policy: ExecCapturePolicy,
|
||||
pub sandbox: SandboxType,
|
||||
pub windows_sandbox_policy_cwd: AbsolutePathBuf,
|
||||
pub windows_sandbox_policy_cwd: PathUri,
|
||||
pub windows_sandbox_workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
@@ -76,6 +77,7 @@ impl ExecRequest {
|
||||
permission_profile: PermissionProfile,
|
||||
arg0: Option<String>,
|
||||
) -> Self {
|
||||
let cwd = PathUri::from_abs_path(&cwd);
|
||||
let windows_sandbox_policy_cwd = cwd.clone();
|
||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||
permission_profile.to_runtime_permissions();
|
||||
|
||||
@@ -199,7 +199,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
let permission_profile = PermissionProfile::Disabled;
|
||||
let exec_env = ExecRequest {
|
||||
command: exec_command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
env: exec_env_map,
|
||||
exec_server_env_config: None,
|
||||
// `/shell` is the explicit full-access escape hatch, so it must not
|
||||
@@ -210,7 +210,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
expiration: USER_SHELL_TIMEOUT_MS.into(),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::None,
|
||||
windows_sandbox_policy_cwd: cwd.clone(),
|
||||
windows_sandbox_policy_cwd: cwd.clone().into(),
|
||||
windows_sandbox_workspace_roots: turn_context.config.effective_workspace_roots(),
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_protocol::protocol::PatchApplyStatus;
|
||||
use codex_protocol::protocol::TurnDiffEvent;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -133,7 +134,7 @@ pub(crate) enum ToolEmitter {
|
||||
},
|
||||
UnifiedExec {
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
source: ExecCommandSource,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
process_id: Option<String>,
|
||||
@@ -165,7 +166,7 @@ impl ToolEmitter {
|
||||
|
||||
pub fn unified_exec(
|
||||
command: &[String],
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
source: ExecCommandSource,
|
||||
process_id: Option<String>,
|
||||
) -> Self {
|
||||
@@ -320,11 +321,15 @@ impl ToolEmitter {
|
||||
},
|
||||
stage,
|
||||
) => {
|
||||
// TODO(anp): Migrate exec command protocol events to PathUri.
|
||||
let Ok(cwd) = cwd.to_abs_path() else {
|
||||
return;
|
||||
};
|
||||
emit_exec_stage(
|
||||
ctx,
|
||||
ExecCommandInput::new(
|
||||
command,
|
||||
cwd,
|
||||
&cwd,
|
||||
parsed_cmd,
|
||||
*source,
|
||||
/*interaction_input*/ None,
|
||||
|
||||
@@ -205,16 +205,19 @@ fn format_update_chunks_for_progress(chunks: &[codex_apply_patch::UpdateFileChun
|
||||
|
||||
fn file_paths_for_action(action: &ApplyPatchAction) -> Vec<AbsolutePathBuf> {
|
||||
let mut keys = Vec::new();
|
||||
let cwd = &action.cwd;
|
||||
// TODO(anp): Migrate permission path accounting to PathUri.
|
||||
let Ok(cwd) = action.cwd.to_abs_path() else {
|
||||
return keys;
|
||||
};
|
||||
|
||||
for (path, change) in action.changes() {
|
||||
if let Some(key) = to_abs_path(cwd, path) {
|
||||
if let Some(key) = to_abs_path(&cwd, path) {
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
if let ApplyPatchFileChange::Update { move_path, .. } = change
|
||||
&& let Some(dest) = move_path
|
||||
&& let Some(key) = to_abs_path(cwd, dest)
|
||||
&& let Some(key) = to_abs_path(&cwd, dest)
|
||||
{
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,13 @@ use crate::unified_exec::generate_chunk_id;
|
||||
use codex_features::Feature;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
|
||||
use super::super::shell_spec::CommandToolOptions;
|
||||
use super::super::shell_spec::create_exec_command_tool_with_environment_id;
|
||||
@@ -129,33 +133,68 @@ impl ExecCommandHandler {
|
||||
"unified exec is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
// TODO(anp): Resolve tool paths using the selected environment's native path convention
|
||||
// so unified exec can support relative paths in foreign environments.
|
||||
let native_environment_cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"environment cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
))
|
||||
})?;
|
||||
let native_environment_cwd = turn_environment.cwd().clone();
|
||||
let cwd = environment_args
|
||||
.workdir
|
||||
.as_deref()
|
||||
.filter(|workdir| !workdir.is_empty())
|
||||
.map_or_else(
|
||||
|| native_environment_cwd.clone(),
|
||||
|| Ok(native_environment_cwd.clone()),
|
||||
|workdir| native_environment_cwd.join(workdir),
|
||||
);
|
||||
)
|
||||
.map_err(|err| FunctionCallError::RespondToModel(err.to_string()))?;
|
||||
let environment = Arc::clone(&turn_environment.environment);
|
||||
let fs = environment.get_filesystem();
|
||||
let args: ExecCommandArgs = parse_arguments_with_base_path(&arguments, &cwd)?;
|
||||
|
||||
// A foreign cwd cannot seed the AbsolutePathBufGuard used to resolve relative paths in the
|
||||
// permissions config below. Consult the configured platform-sandbox requirement before
|
||||
// deciding whether parsing may continue without that base path.
|
||||
let sandbox = SandboxManager::new().select_initial(
|
||||
&turn.file_system_sandbox_policy(),
|
||||
turn.network_sandbox_policy(),
|
||||
SandboxablePreference::Auto,
|
||||
turn.windows_sandbox_level,
|
||||
turn.network.is_some(),
|
||||
);
|
||||
// `to_abs_path()` alone cannot identify foreign drive paths: `file:///C:/repo` is
|
||||
// representable as `/C:/repo` on POSIX. Require the inferred convention to match too.
|
||||
let cwd_uses_native_convention =
|
||||
cwd.infer_path_convention() == Some(PathConvention::native());
|
||||
// TODO(anp): Remove this parsing split once sandboxing supports foreign paths.
|
||||
let native_cwd = match cwd.to_abs_path() {
|
||||
Ok(cwd) if cwd_uses_native_convention => Some(cwd),
|
||||
_ if sandbox == SandboxType::None => None,
|
||||
Err(err) => return Err(FunctionCallError::RespondToModel(err.to_string())),
|
||||
Ok(_) => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"path URI `{cwd}` does not use the host's native {} path convention",
|
||||
PathConvention::native()
|
||||
)));
|
||||
}
|
||||
};
|
||||
let args: ExecCommandArgs = match native_cwd.as_ref() {
|
||||
Some(native_cwd) => {
|
||||
// The base path only resolves paths nested in the permissions config types.
|
||||
parse_arguments_with_base_path(&arguments, native_cwd)?
|
||||
}
|
||||
None => {
|
||||
// Parsing without a base only skips relative-path resolution inside the
|
||||
// permissions config. That is safe only for a truly unsandboxed attempt;
|
||||
// sandboxed attempts fall through and return the conversion error below.
|
||||
parse_arguments(&arguments)?
|
||||
}
|
||||
};
|
||||
let hook_command = args.cmd.clone();
|
||||
maybe_emit_implicit_skill_invocation(
|
||||
session.as_ref(),
|
||||
context.turn.as_ref(),
|
||||
&hook_command,
|
||||
&cwd,
|
||||
)
|
||||
.await;
|
||||
// TODO(anp) wire PathUri through implicit skills instead of skipping on foreign paths
|
||||
if let Some(native_cwd) = native_cwd.as_ref() {
|
||||
maybe_emit_implicit_skill_invocation(
|
||||
session.as_ref(),
|
||||
context.turn.as_ref(),
|
||||
&hook_command,
|
||||
native_cwd,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
let process_id = manager.allocate_process_id().await;
|
||||
let shell_mode =
|
||||
shell_mode_for_environment(&turn.unified_exec_shell_mode, environment.as_ref());
|
||||
@@ -191,10 +230,12 @@ impl ExecCommandHandler {
|
||||
let exec_permission_approvals_enabled =
|
||||
session.features().enabled(Feature::ExecPermissionApprovals);
|
||||
let requested_additional_permissions = additional_permissions.clone();
|
||||
// TODO(anp): Make permission matching operate on PathUri for remote environments.
|
||||
let permission_cwd = native_cwd.as_ref().unwrap_or(&turn.config.cwd);
|
||||
let effective_additional_permissions = apply_granted_turn_permissions(
|
||||
context.session.as_ref(),
|
||||
&turn_environment.environment_id,
|
||||
cwd.as_path(),
|
||||
permission_cwd.as_path(),
|
||||
sandbox_permissions,
|
||||
additional_permissions,
|
||||
)
|
||||
@@ -234,7 +275,7 @@ impl ExecCommandHandler {
|
||||
effective_additional_permissions.sandbox_permissions,
|
||||
effective_additional_permissions.additional_permissions,
|
||||
effective_additional_permissions.permissions_preapproved,
|
||||
&cwd,
|
||||
permission_cwd,
|
||||
)
|
||||
},
|
||||
|permissions| Ok(Some(permissions)),
|
||||
@@ -246,18 +287,20 @@ impl ExecCommandHandler {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(output) = intercept_apply_patch(
|
||||
&command,
|
||||
&cwd,
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
context.session.clone(),
|
||||
context.turn.clone(),
|
||||
Some(&tracker),
|
||||
&context.call_id,
|
||||
"exec_command",
|
||||
)
|
||||
.await?
|
||||
// TODO(anp) intercept apply_patch properly when cwd is a foreign path
|
||||
if let Some(native_cwd) = native_cwd.as_ref()
|
||||
&& let Some(output) = intercept_apply_patch(
|
||||
&command,
|
||||
native_cwd,
|
||||
fs.as_ref(),
|
||||
turn_environment.clone(),
|
||||
context.session.clone(),
|
||||
context.turn.clone(),
|
||||
Some(&tracker),
|
||||
&context.call_id,
|
||||
"exec_command",
|
||||
)
|
||||
.await?
|
||||
{
|
||||
manager.release_process_id(process_id).await;
|
||||
return Ok(boxed_tool_output(ExecCommandToolOutput {
|
||||
|
||||
@@ -239,8 +239,10 @@ impl ToolOrchestrator {
|
||||
// Platform-specific flag gating is handled by SandboxManager::select_initial.
|
||||
let use_legacy_landlock = turn_ctx.config.features.use_legacy_landlock();
|
||||
#[allow(deprecated)]
|
||||
let sandbox_cwd = tool.sandbox_cwd(req).unwrap_or(&turn_ctx.cwd);
|
||||
let sandbox_policy_cwd = PathUri::from_abs_path(sandbox_cwd);
|
||||
let sandbox_policy_cwd = tool
|
||||
.sandbox_cwd(req)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| PathUri::from_abs_path(&turn_ctx.cwd));
|
||||
let workspace_roots = turn_ctx.config.effective_workspace_roots();
|
||||
let initial_attempt = SandboxAttempt {
|
||||
sandbox: initial_sandbox,
|
||||
|
||||
@@ -78,13 +78,15 @@ impl ApplyPatchRuntime {
|
||||
fn build_guardian_review_request(
|
||||
req: &ApplyPatchRequest,
|
||||
call_id: &str,
|
||||
) -> GuardianApprovalRequest {
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
) -> std::io::Result<GuardianApprovalRequest> {
|
||||
// TODO(anp): Remove this conversion once the guardian API supports PathUri.
|
||||
let cwd = req.action.cwd.to_abs_path()?;
|
||||
Ok(GuardianApprovalRequest::ApplyPatch {
|
||||
id: call_id.to_string(),
|
||||
cwd: req.action.cwd.clone(),
|
||||
cwd,
|
||||
files: req.file_paths.clone(),
|
||||
patch: req.action.patch.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn file_system_sandbox_context_for_attempt(
|
||||
@@ -149,7 +151,16 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id);
|
||||
let action = match ApplyPatchRuntime::build_guardian_review_request(
|
||||
req,
|
||||
ctx.call_id,
|
||||
) {
|
||||
Ok(action) => action,
|
||||
Err(err) => {
|
||||
tracing::error!(cwd = %req.action.cwd, %err, "guardian apply_patch cwd is not host-native");
|
||||
return ReviewDecision::Abort;
|
||||
}
|
||||
};
|
||||
return review_approval_request(session, turn, review_id, action, retry_reason)
|
||||
.await;
|
||||
}
|
||||
@@ -219,7 +230,7 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
}
|
||||
|
||||
impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRuntime {
|
||||
fn sandbox_cwd<'a>(&self, req: &'a ApplyPatchRequest) -> Option<&'a AbsolutePathBuf> {
|
||||
fn sandbox_cwd<'a>(&self, req: &'a ApplyPatchRequest) -> Option<&'a PathUri> {
|
||||
Some(&req.action.cwd)
|
||||
}
|
||||
|
||||
@@ -234,9 +245,15 @@ impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRunti
|
||||
let sandbox = Self::file_system_sandbox_context_for_attempt(req, attempt);
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
// TODO(anp): Teach apply_patch to operate on PathUri directly.
|
||||
let cwd = req
|
||||
.action
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
let result = codex_apply_patch::apply_patch(
|
||||
&req.action.patch,
|
||||
&req.action.cwd,
|
||||
&cwd,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
fs.as_ref(),
|
||||
|
||||
@@ -54,7 +54,7 @@ async fn guardian_review_request_includes_patch_context() {
|
||||
.join("guardian-apply-patch-test.txt")
|
||||
.abs();
|
||||
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
|
||||
let expected_cwd = action.cwd.clone();
|
||||
let expected_cwd = action.cwd.to_abs_path().expect("native patch cwd");
|
||||
let expected_patch = action.patch.clone();
|
||||
let request = ApplyPatchRequest {
|
||||
turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID),
|
||||
@@ -74,7 +74,8 @@ async fn guardian_review_request_includes_patch_context() {
|
||||
permissions_preapproved: false,
|
||||
};
|
||||
|
||||
let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1");
|
||||
let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1")
|
||||
.expect("native guardian request cwd");
|
||||
|
||||
assert_eq!(
|
||||
guardian_request,
|
||||
|
||||
@@ -129,10 +129,10 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
|
||||
)
|
||||
.expect("prepare exec request");
|
||||
|
||||
assert_eq!(exec_request.cwd, command_cwd);
|
||||
assert_eq!(exec_request.cwd, PathUri::from_abs_path(&command_cwd));
|
||||
assert_eq!(
|
||||
exec_request.windows_sandbox_policy_cwd,
|
||||
native_sandbox_policy_cwd
|
||||
PathUri::from_abs_path(&native_sandbox_policy_cwd)
|
||||
);
|
||||
assert_eq!(exec_request.network, None);
|
||||
for key in PROXY_ENV_KEYS {
|
||||
|
||||
@@ -172,6 +172,14 @@ pub(super) async fn try_run_zsh_fork(
|
||||
let exec_policy = Arc::new(RwLock::new(
|
||||
ctx.session.services.exec_policy.current().as_ref().clone(),
|
||||
));
|
||||
// TODO(anp): Keep PathUri through the shell escalation boundary.
|
||||
let sandbox_cwd = sandbox_cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
// TODO(anp): Keep PathUri through the shell sandbox policy boundary.
|
||||
let sandbox_policy_cwd = sandbox_policy_cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
let command_executor = CoreShellCommandExecutor {
|
||||
command,
|
||||
cwd: sandbox_cwd,
|
||||
@@ -274,9 +282,19 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
let exec_policy = Arc::new(RwLock::new(
|
||||
ctx.session.services.exec_policy.current().as_ref().clone(),
|
||||
));
|
||||
// TODO(anp): Keep PathUri through the zsh-fork executor boundary.
|
||||
let cwd = exec_request
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
// TODO(anp): Keep PathUri through the zsh-fork sandbox policy boundary.
|
||||
let sandbox_policy_cwd = exec_request
|
||||
.windows_sandbox_policy_cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
let command_executor = CoreShellCommandExecutor {
|
||||
command: exec_request.command.clone(),
|
||||
cwd: exec_request.cwd.clone(),
|
||||
cwd,
|
||||
permission_profile: exec_request.permission_profile.clone(),
|
||||
file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
|
||||
network_sandbox_policy: exec_request.network_sandbox_policy,
|
||||
@@ -285,7 +303,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
network: exec_request.network.clone(),
|
||||
windows_sandbox_level: exec_request.windows_sandbox_level,
|
||||
arg0: exec_request.arg0.clone(),
|
||||
sandbox_policy_cwd: exec_request.windows_sandbox_policy_cwd.clone(),
|
||||
sandbox_policy_cwd,
|
||||
windows_sandbox_workspace_roots: exec_request.windows_sandbox_workspace_roots.clone(),
|
||||
codex_linux_sandbox_exe: ctx.turn.config.codex_linux_sandbox_exe.clone(),
|
||||
use_legacy_landlock: ctx.turn.config.features.use_legacy_landlock(),
|
||||
@@ -857,14 +875,14 @@ impl CoreShellCommandExecutor {
|
||||
let result = crate::sandboxing::execute_exec_request_with_after_spawn(
|
||||
crate::sandboxing::ExecRequest {
|
||||
command: self.command.clone(),
|
||||
cwd: self.cwd.clone(),
|
||||
cwd: self.cwd.clone().into(),
|
||||
env: exec_env,
|
||||
exec_server_env_config: None,
|
||||
network: self.network.clone(),
|
||||
expiration: ExecExpiration::Cancellation(cancel_rx),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
sandbox: self.sandbox,
|
||||
windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone(),
|
||||
windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone().into(),
|
||||
windows_sandbox_workspace_roots: self.windows_sandbox_workspace_roots.clone(),
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -1010,7 +1028,8 @@ impl CoreShellCommandExecutor {
|
||||
|
||||
Ok(PreparedExec {
|
||||
command: exec_request.command,
|
||||
cwd: exec_request.cwd.to_path_buf(),
|
||||
// TODO(anp): Keep PathUri through the execve-wrapper boundary.
|
||||
cwd: exec_request.cwd.to_abs_path()?.to_path_buf(),
|
||||
env: exec_request.env,
|
||||
arg0: exec_request.arg0,
|
||||
})
|
||||
|
||||
@@ -21,7 +21,6 @@ use crate::tools::network_approval::NetworkApprovalSpec;
|
||||
use crate::tools::runtimes::RuntimePathPrepends;
|
||||
#[cfg(unix)]
|
||||
use crate::tools::runtimes::apply_zsh_fork_path_prepend;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
use crate::tools::runtimes::disable_powershell_profile_for_elevated_windows_sandbox;
|
||||
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
|
||||
use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
|
||||
@@ -47,13 +46,15 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_shell_command::powershell::prefix_powershell_script_with_utf8;
|
||||
use codex_tools::UnifiedExecShellMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use futures::future::BoxFuture;
|
||||
use std::collections::HashMap;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::error;
|
||||
|
||||
/// Request payload used by the unified-exec runtime after approvals and
|
||||
/// sandbox preferences have been resolved for the current turn.
|
||||
@@ -63,8 +64,8 @@ pub struct UnifiedExecRequest {
|
||||
pub shell_type: ShellType,
|
||||
pub hook_command: String,
|
||||
pub process_id: i32,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub sandbox_cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub sandbox_cwd: PathUri,
|
||||
pub turn_environment: TurnEnvironment,
|
||||
pub env: HashMap<String, String>,
|
||||
pub exec_server_env_config: Option<ExecServerEnvConfig>,
|
||||
@@ -85,7 +86,7 @@ pub struct UnifiedExecRequest {
|
||||
pub struct UnifiedExecApprovalKey {
|
||||
pub environment_id: String,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub tty: bool,
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
@@ -111,6 +112,24 @@ fn unified_exec_options(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_unified_exec_sandbox_command(
|
||||
command: &[String],
|
||||
cwd: &PathUri,
|
||||
env: &HashMap<String, String>,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
) -> Result<SandboxCommand, ToolError> {
|
||||
let (program, args) = command
|
||||
.split_first()
|
||||
.ok_or_else(|| ToolError::Rejected("command args are empty".to_string()))?;
|
||||
Ok(SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
cwd: cwd.clone(),
|
||||
env: env.clone(),
|
||||
additional_permissions,
|
||||
})
|
||||
}
|
||||
|
||||
impl<'a> UnifiedExecRuntime<'a> {
|
||||
/// Creates a runtime bound to the shared unified-exec process manager.
|
||||
pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self {
|
||||
@@ -155,12 +174,20 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let command = req.command.clone();
|
||||
let cwd = req.cwd.clone();
|
||||
let environment_id = Some(req.turn_environment.environment_id.clone());
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
let native_cwd = match req.cwd.to_abs_path() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
// TODO(anp) make sandboxing work for foreign OSes, in the meantime this should
|
||||
// be impossible for single-OS app-servers
|
||||
error!(cwd = %req.cwd, ?e, "got non-native path in start_approval_async");
|
||||
return ReviewDecision::Abort;
|
||||
}
|
||||
};
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return review_approval_request(
|
||||
session,
|
||||
@@ -169,7 +196,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
GuardianApprovalRequest::ExecCommand {
|
||||
id: call_id,
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
cwd: native_cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
@@ -188,7 +215,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
/*approval_id*/ None,
|
||||
environment_id,
|
||||
command,
|
||||
cwd.clone(),
|
||||
native_cwd,
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
@@ -226,7 +253,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
}
|
||||
|
||||
impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRuntime<'a> {
|
||||
fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b AbsolutePathBuf> {
|
||||
fn sandbox_cwd<'b>(&self, req: &'b UnifiedExecRequest) -> Option<&'b PathUri> {
|
||||
Some(&req.sandbox_cwd)
|
||||
}
|
||||
|
||||
@@ -249,7 +276,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
call_id: ctx.call_id.clone(),
|
||||
tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
|
||||
command: req.command.clone(),
|
||||
cwd: req.cwd.clone(),
|
||||
cwd: req.cwd.to_abs_path().ok()?,
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
@@ -272,7 +299,17 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.shell
|
||||
.as_ref()
|
||||
.unwrap_or(session_shell.as_ref());
|
||||
let shell_snapshot_location = req.turn_environment.shell_snapshot(&req.cwd);
|
||||
let environment_is_remote = req.turn_environment.environment.is_remote();
|
||||
let shell_snapshot_location = if environment_is_remote {
|
||||
None
|
||||
} else {
|
||||
// TODO(anp): Make shell snapshot lookup accept PathUri.
|
||||
let native_cwd = req
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?;
|
||||
req.turn_environment.shell_snapshot(&native_cwd)
|
||||
};
|
||||
let (file_system_sandbox_policy, _) = attempt.permissions.to_runtime_permissions();
|
||||
let launch_sandbox_permissions = sandbox_permissions_preserving_denied_reads(
|
||||
req.sandbox_permissions,
|
||||
@@ -286,7 +323,6 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
if let Some(network) = managed_network {
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
let environment_is_remote = req.turn_environment.environment.is_remote();
|
||||
let explicit_env_overrides = req.explicit_env_overrides.clone();
|
||||
#[cfg(unix)]
|
||||
let runtime_path_prepends = {
|
||||
@@ -333,14 +369,18 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
};
|
||||
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let command =
|
||||
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())
|
||||
.map_err(|error| match error {
|
||||
ToolError::Rejected(_) => {
|
||||
ToolError::Rejected("missing command line for PTY".to_string())
|
||||
}
|
||||
error @ ToolError::Codex(_) => error,
|
||||
})?;
|
||||
let command = build_unified_exec_sandbox_command(
|
||||
&command,
|
||||
&req.cwd,
|
||||
&env,
|
||||
req.additional_permissions.clone(),
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
ToolError::Rejected(_) => {
|
||||
ToolError::Rejected("missing command line for PTY".to_string())
|
||||
}
|
||||
error @ ToolError::Codex(_) => error,
|
||||
})?;
|
||||
let options = unified_exec_options(attempt.network_denial_cancellation_token.clone());
|
||||
let mut exec_env = attempt
|
||||
.env_for(command, options, managed_network)
|
||||
@@ -389,14 +429,18 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
}
|
||||
}
|
||||
}
|
||||
let command =
|
||||
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())
|
||||
.map_err(|error| match error {
|
||||
ToolError::Rejected(_) => {
|
||||
ToolError::Rejected("missing command line for PTY".to_string())
|
||||
}
|
||||
error @ ToolError::Codex(_) => error,
|
||||
})?;
|
||||
let command = build_unified_exec_sandbox_command(
|
||||
&command,
|
||||
&req.cwd,
|
||||
&env,
|
||||
req.additional_permissions.clone(),
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
ToolError::Rejected(_) => {
|
||||
ToolError::Rejected("missing command line for PTY".to_string())
|
||||
}
|
||||
error @ ToolError::Codex(_) => error,
|
||||
})?;
|
||||
let options = unified_exec_options(attempt.network_denial_cancellation_token.clone());
|
||||
let mut exec_env = attempt
|
||||
.env_for(command, options, managed_network)
|
||||
@@ -431,16 +475,17 @@ mod tests {
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_tools::ZshForkConfig;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn test_turn_environment(cwd: AbsolutePathBuf) -> TurnEnvironment {
|
||||
fn test_turn_environment(cwd: PathUri) -> TurnEnvironment {
|
||||
TurnEnvironment::new(
|
||||
LOCAL_ENVIRONMENT_ID.to_string(),
|
||||
Arc::new(Environment::default_for_tests()),
|
||||
PathUri::from_abs_path(&cwd),
|
||||
cwd,
|
||||
/*shell*/ None,
|
||||
)
|
||||
}
|
||||
@@ -501,9 +546,9 @@ mod tests {
|
||||
shell_type: ShellType::Sh,
|
||||
hook_command: "pwd".to_string(),
|
||||
process_id: 1000,
|
||||
cwd,
|
||||
sandbox_cwd: sandbox_cwd.clone(),
|
||||
turn_environment: test_turn_environment(sandbox_cwd.clone()),
|
||||
cwd: cwd.into(),
|
||||
sandbox_cwd: sandbox_cwd.clone().into(),
|
||||
turn_environment: test_turn_environment(sandbox_cwd.clone().into()),
|
||||
env: HashMap::new(),
|
||||
exec_server_env_config: None,
|
||||
explicit_env_overrides: HashMap::new(),
|
||||
@@ -520,7 +565,10 @@ mod tests {
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(runtime.sandbox_cwd(&request), Some(&sandbox_cwd));
|
||||
assert_eq!(
|
||||
runtime.sandbox_cwd(&request),
|
||||
Some(&PathUri::from_abs_path(&sandbox_cwd))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -600,9 +648,9 @@ mod tests {
|
||||
shell_type: ShellType::Zsh,
|
||||
hook_command: "echo hi".to_string(),
|
||||
process_id: 1000,
|
||||
cwd: cwd.clone(),
|
||||
sandbox_cwd: cwd.clone(),
|
||||
turn_environment: test_turn_environment(cwd),
|
||||
cwd: cwd.clone().into(),
|
||||
sandbox_cwd: cwd.clone().into(),
|
||||
turn_environment: test_turn_environment(cwd.into()),
|
||||
env: HashMap::new(),
|
||||
exec_server_env_config: None,
|
||||
explicit_env_overrides: HashMap::new(),
|
||||
|
||||
@@ -394,7 +394,7 @@ pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
|
||||
None
|
||||
}
|
||||
|
||||
fn sandbox_cwd<'a>(&self, _req: &'a Req) -> Option<&'a AbsolutePathBuf> {
|
||||
fn sandbox_cwd<'a>(&self, _req: &'a Req) -> Option<&'a PathUri> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ExecOutputStream;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
pub(crate) const TRAILING_OUTPUT_GRACE: Duration = Duration::from_millis(100);
|
||||
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
process_id: i32,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
started_at: Instant,
|
||||
@@ -197,7 +197,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
@@ -242,7 +242,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -23,6 +24,8 @@ pub(crate) enum UnifiedExecError {
|
||||
message: String,
|
||||
output: ExecToolCallOutput,
|
||||
},
|
||||
#[error("{path} is not valid on {}", std::env::consts::OS)]
|
||||
ForeignPath { path: PathUri },
|
||||
}
|
||||
|
||||
impl UnifiedExecError {
|
||||
|
||||
@@ -30,8 +30,8 @@ use std::sync::Weak;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_tools::UnifiedExecShellMode;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use rand::Rng;
|
||||
use rand::rng;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -96,8 +96,8 @@ pub(crate) struct ExecCommandRequest {
|
||||
pub process_id: i32,
|
||||
pub yield_time_ms: u64,
|
||||
pub max_output_tokens: Option<usize>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub sandbox_cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub sandbox_cwd: PathUri,
|
||||
pub turn_environment: TurnEnvironment,
|
||||
pub shell_mode: UnifiedExecShellMode,
|
||||
pub network: Option<NetworkProxy>,
|
||||
@@ -156,7 +156,7 @@ struct ProcessEntry {
|
||||
process: Arc<UnifiedExecProcess>,
|
||||
call_id: String,
|
||||
process_id: i32,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
initial_exec_command_active: Arc<std::sync::atomic::AtomicBool>,
|
||||
hook_command: String,
|
||||
tty: bool,
|
||||
|
||||
@@ -20,6 +20,7 @@ use codex_exec_server::StartedExecProcess;
|
||||
use codex_exec_server::WriteResponse;
|
||||
use codex_exec_server::WriteStatus;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use core_test_support::get_remote_test_env;
|
||||
@@ -128,7 +129,7 @@ async fn exec_command_with_tty(
|
||||
process: Arc::clone(&process),
|
||||
call_id: context.call_id.clone(),
|
||||
process_id,
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
initial_exec_command_active: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
hook_command: cmd.to_string(),
|
||||
tty,
|
||||
@@ -370,7 +371,7 @@ async fn unified_exec_persists_across_requests() -> anyhow::Result<()> {
|
||||
item_id: "call".to_string(),
|
||||
process_id: process_id.to_string(),
|
||||
command: "bash -i".to_string(),
|
||||
cwd,
|
||||
cwd: cwd.into(),
|
||||
}]
|
||||
);
|
||||
|
||||
@@ -674,7 +675,7 @@ async fn terminating_initial_exec_command_rechecks_initial_response_state() -> a
|
||||
process,
|
||||
call_id: "call".to_string(),
|
||||
process_id,
|
||||
cwd,
|
||||
cwd: cwd.into(),
|
||||
initial_exec_command_active: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
hook_command: "sleep 60".to_string(),
|
||||
tty: true,
|
||||
@@ -747,7 +748,7 @@ async fn terminating_during_stdin_poll_returns_exited_response() -> anyhow::Resu
|
||||
process: Arc::clone(&process),
|
||||
call_id: "call".to_string(),
|
||||
process_id,
|
||||
cwd,
|
||||
cwd: cwd.into(),
|
||||
initial_exec_command_active: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
hook_command: "sleep 60".to_string(),
|
||||
tty: true,
|
||||
|
||||
@@ -55,7 +55,6 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_tools::ToolName;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
@@ -157,7 +156,7 @@ fn exec_server_params_for_request(
|
||||
codex_exec_server::ExecParams {
|
||||
process_id: exec_server_process_id(process_id).into(),
|
||||
argv: request.command.clone(),
|
||||
cwd: PathUri::from_abs_path(&request.cwd),
|
||||
cwd: request.cwd.clone(),
|
||||
env_policy,
|
||||
env,
|
||||
tty,
|
||||
@@ -294,7 +293,7 @@ async fn emit_failed_initial_exec_end_if_unstored(
|
||||
process_started_alive: bool,
|
||||
context: &UnifiedExecContext,
|
||||
request: &ExecCommandRequest,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
message: String,
|
||||
@@ -841,7 +840,7 @@ impl UnifiedExecProcessManager {
|
||||
context: &UnifiedExecContext,
|
||||
command: &[String],
|
||||
hook_command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
started_at: Instant,
|
||||
process_id: i32,
|
||||
tty: bool,
|
||||
@@ -899,6 +898,14 @@ impl UnifiedExecProcessManager {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
|
||||
// TODO(anp): Keep PathUri through the Windows sandbox launch boundary.
|
||||
let native_cwd =
|
||||
request
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|_| UnifiedExecError::ForeignPath {
|
||||
path: request.cwd.clone(),
|
||||
})?;
|
||||
let codex_home = crate::config::find_codex_home().map_err(|err| {
|
||||
UnifiedExecError::create_process(format!(
|
||||
"windows sandbox: failed to resolve codex_home: {err}"
|
||||
@@ -933,7 +940,7 @@ impl UnifiedExecProcessManager {
|
||||
request.windows_sandbox_workspace_roots.as_slice(),
|
||||
codex_home.as_ref(),
|
||||
request.command.clone(),
|
||||
request.cwd.as_path(),
|
||||
native_cwd.as_path(),
|
||||
request.env.clone(),
|
||||
request.network.is_some(),
|
||||
None,
|
||||
@@ -955,7 +962,7 @@ impl UnifiedExecProcessManager {
|
||||
request.windows_sandbox_workspace_roots.as_slice(),
|
||||
codex_home.as_ref(),
|
||||
request.command.clone(),
|
||||
request.cwd.as_path(),
|
||||
native_cwd.as_path(),
|
||||
request.env.clone(),
|
||||
None,
|
||||
&additional_deny_read_paths,
|
||||
@@ -991,6 +998,14 @@ impl UnifiedExecProcessManager {
|
||||
return UnifiedExecProcess::from_exec_server_started(started, request.sandbox).await;
|
||||
}
|
||||
|
||||
// TODO(anp): Keep PathUri through the local PTY/process launch boundary.
|
||||
let native_cwd = request
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|_| UnifiedExecError::ForeignPath {
|
||||
path: request.cwd.clone(),
|
||||
})?;
|
||||
|
||||
let (program, args) = request
|
||||
.command
|
||||
.split_first()
|
||||
@@ -999,7 +1014,7 @@ impl UnifiedExecProcessManager {
|
||||
codex_utils_pty::pty::spawn_process_with_inherited_fds(
|
||||
program,
|
||||
args,
|
||||
request.cwd.as_path(),
|
||||
native_cwd.as_path(),
|
||||
&request.env,
|
||||
&request.arg0,
|
||||
codex_utils_pty::TerminalSize::default(),
|
||||
@@ -1010,7 +1025,7 @@ impl UnifiedExecProcessManager {
|
||||
codex_utils_pty::pipe::spawn_process_no_stdin_with_inherited_fds(
|
||||
program,
|
||||
args,
|
||||
request.cwd.as_path(),
|
||||
native_cwd.as_path(),
|
||||
&request.env,
|
||||
&request.arg0,
|
||||
&inherited_fds,
|
||||
@@ -1026,7 +1041,7 @@ impl UnifiedExecProcessManager {
|
||||
pub(super) async fn open_session_with_sandbox(
|
||||
&self,
|
||||
request: &ExecCommandRequest,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
context: &UnifiedExecContext,
|
||||
) -> Result<(UnifiedExecProcess, Option<DeferredNetworkApproval>), UnifiedExecError> {
|
||||
let local_policy_env = create_env(
|
||||
|
||||
@@ -78,7 +78,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
let permission_profile = codex_protocol::models::PermissionProfile::Disabled;
|
||||
let request = ExecRequest {
|
||||
command: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
cwd: cwd.clone(),
|
||||
cwd: cwd.clone().into(),
|
||||
env: HashMap::from([
|
||||
("HOME".to_string(), "/client-home".to_string()),
|
||||
("PATH".to_string(), "/sandbox-path".to_string()),
|
||||
@@ -101,7 +101,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
expiration: crate::exec::ExecExpiration::DefaultTimeout,
|
||||
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
|
||||
sandbox: codex_sandboxing::SandboxType::None,
|
||||
windows_sandbox_policy_cwd: cwd.clone(),
|
||||
windows_sandbox_policy_cwd: cwd.clone().into(),
|
||||
windows_sandbox_workspace_roots: vec![cwd],
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -116,7 +116,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);
|
||||
|
||||
assert_eq!(params.process_id.as_str(), "123");
|
||||
assert_eq!(params.cwd, PathUri::from_abs_path(&request.cwd));
|
||||
assert_eq!(params.cwd, request.cwd);
|
||||
assert!(params.env_policy.is_some());
|
||||
assert_eq!(
|
||||
params.env,
|
||||
@@ -200,9 +200,9 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
|
||||
yield_time_ms: 1000,
|
||||
max_output_tokens: None,
|
||||
#[allow(deprecated)]
|
||||
cwd: turn.cwd.clone(),
|
||||
cwd: turn.cwd.clone().into(),
|
||||
#[allow(deprecated)]
|
||||
sandbox_cwd: turn.cwd.clone(),
|
||||
sandbox_cwd: turn.cwd.clone().into(),
|
||||
turn_environment: turn
|
||||
.environments
|
||||
.primary()
|
||||
@@ -229,7 +229,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
|
||||
&context,
|
||||
&request,
|
||||
#[allow(deprecated)]
|
||||
turn.cwd.clone(),
|
||||
turn.cwd.clone().into(),
|
||||
transcript,
|
||||
"PRE_DENIAL_MARKER".to_string(),
|
||||
"Network access denied".to_string(),
|
||||
|
||||
@@ -20,7 +20,6 @@ use codex_features::Feature;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandStatus;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelections;
|
||||
@@ -58,6 +57,9 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
let arguments = serde_json::to_string(&json!({
|
||||
"cmd": COMMAND,
|
||||
"login": false,
|
||||
// An absolute foreign workdir should replace the selected environment cwd and
|
||||
// reach exec-server without conversion to the host path convention.
|
||||
"workdir": r"C:\windows",
|
||||
"yield_time_ms": 10_000,
|
||||
}))?;
|
||||
let response_mock = mount_sse_sequence(
|
||||
@@ -125,6 +127,10 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
|
||||
// TODO(anp): Re-enable these event assertions once exec command events retain a
|
||||
// PathUri cwd. Today the host-native event conversion drops begin/end events for a
|
||||
// foreign cwd.
|
||||
/*
|
||||
let mut begin = None;
|
||||
let mut end = None;
|
||||
let mut turn_complete = false;
|
||||
@@ -152,13 +158,16 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> {
|
||||
"unexpected command: {:?}",
|
||||
begin.command
|
||||
);
|
||||
assert_eq!(
|
||||
&begin.command[1..],
|
||||
["-NoProfile", "-Command", COMMAND]
|
||||
);
|
||||
assert_eq!(&begin.command[1..], ["-NoProfile", "-Command", COMMAND]);
|
||||
|
||||
let end = end.context("exec_command should emit an end event")?;
|
||||
assert_eq!((end.exit_code, end.status), (0, ExecCommandStatus::Completed));
|
||||
*/
|
||||
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let request = response_mock
|
||||
.last_request()
|
||||
|
||||
@@ -341,6 +341,8 @@ fn spawn_command(
|
||||
#[cfg(not(unix))]
|
||||
let _ = arg0;
|
||||
command.args(args);
|
||||
// TODO(anp): Keep PathUri through the filesystem helper launch boundary.
|
||||
let cwd = cwd.to_abs_path().map_err(io_error)?;
|
||||
command.current_dir(cwd.as_path());
|
||||
command.env_clear();
|
||||
command.envs(env);
|
||||
|
||||
@@ -60,7 +60,12 @@ async fn spawn_command_under_sandbox(
|
||||
child.arg0(arg0);
|
||||
}
|
||||
child.args(args);
|
||||
child.current_dir(exec_request.cwd);
|
||||
// TODO(anp): Keep PathUri through the macOS sandbox process launch boundary.
|
||||
let native_cwd = exec_request
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|err| io::Error::other(err.to_string()))?;
|
||||
child.current_dir(native_cwd);
|
||||
child.env_clear();
|
||||
child.envs(exec_request.env);
|
||||
|
||||
|
||||
@@ -109,8 +109,8 @@ pub struct SandboxCommand {
|
||||
#[derive(Debug)]
|
||||
pub struct SandboxExecRequest {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub sandbox_policy_cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
pub sandbox_policy_cwd: PathUri,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
pub sandbox: SandboxType,
|
||||
@@ -150,6 +150,52 @@ pub struct SandboxDirectSpawnTransformRequest<'a> {
|
||||
pub workspace_roots: &'a [AbsolutePathBuf],
|
||||
}
|
||||
|
||||
// TODO(anp): Revisit this preparation type once this module's PathUri migration is complete.
|
||||
struct PendingSandboxedExecRequest {
|
||||
native_command_cwd: AbsolutePathBuf,
|
||||
native_sandbox_policy_cwd: AbsolutePathBuf,
|
||||
effective_permission_profile: PermissionProfile,
|
||||
effective_file_system_policy: FileSystemSandboxPolicy,
|
||||
effective_network_policy: NetworkSandboxPolicy,
|
||||
}
|
||||
|
||||
impl PendingSandboxedExecRequest {
|
||||
fn new(
|
||||
command_cwd: &PathUri,
|
||||
sandbox_policy_cwd: &PathUri,
|
||||
effective_permission_profile: PermissionProfile,
|
||||
managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,
|
||||
) -> Result<Self, SandboxTransformError> {
|
||||
// TODO(anp): Move PathUri conversion into the platform sandbox implementations.
|
||||
let native_command_cwd = command_cwd.to_abs_path().map_err(|source| {
|
||||
SandboxTransformError::InvalidCommandCwd {
|
||||
cwd: command_cwd.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let native_sandbox_policy_cwd = sandbox_policy_cwd.to_abs_path().map_err(|source| {
|
||||
SandboxTransformError::InvalidSandboxPolicyCwd {
|
||||
cwd: sandbox_policy_cwd.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let effective_permission_profile = with_managed_mitm_ca_readable_root(
|
||||
effective_permission_profile,
|
||||
managed_mitm_ca_trust_bundle_path,
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
let (effective_file_system_policy, effective_network_policy) =
|
||||
effective_permission_profile.to_runtime_permissions();
|
||||
Ok(Self {
|
||||
native_command_cwd,
|
||||
native_sandbox_policy_cwd,
|
||||
effective_permission_profile,
|
||||
effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SandboxTransformError {
|
||||
InvalidCommandCwd {
|
||||
@@ -266,47 +312,37 @@ impl SandboxManager {
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
} = request;
|
||||
let native_command_cwd = command.cwd.to_abs_path().map_err(|source| {
|
||||
SandboxTransformError::InvalidCommandCwd {
|
||||
cwd: command.cwd.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let native_sandbox_policy_cwd = sandbox_policy_cwd.to_abs_path().map_err(|source| {
|
||||
SandboxTransformError::InvalidSandboxPolicyCwd {
|
||||
cwd: sandbox_policy_cwd.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let additional_permissions = command.additional_permissions.take();
|
||||
let managed_mitm_ca_trust_bundle_path =
|
||||
network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
|
||||
let effective_permission_profile =
|
||||
let base_effective_permission_profile =
|
||||
effective_permission_profile(permissions, additional_permissions.as_ref());
|
||||
let effective_permission_profile = with_managed_mitm_ca_readable_root(
|
||||
effective_permission_profile,
|
||||
let pending_sandboxed_request = PendingSandboxedExecRequest::new(
|
||||
&command.cwd,
|
||||
sandbox_policy_cwd,
|
||||
base_effective_permission_profile.clone(),
|
||||
managed_mitm_ca_trust_bundle_path.as_ref(),
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
let (effective_file_system_policy, effective_network_policy) =
|
||||
effective_permission_profile.to_runtime_permissions();
|
||||
let (base_file_system_policy, base_network_policy) =
|
||||
base_effective_permission_profile.to_runtime_permissions();
|
||||
let mut argv = Vec::with_capacity(1 + command.args.len());
|
||||
argv.push(command.program);
|
||||
argv.extend(command.args.into_iter().map(OsString::from));
|
||||
|
||||
let (argv, arg0_override) = match sandbox {
|
||||
SandboxType::None => (os_argv_to_strings(argv), None),
|
||||
let (argv, arg0_override, pending_sandboxed_request) = match sandbox {
|
||||
SandboxType::None => (os_argv_to_strings(argv), None, None),
|
||||
#[cfg(target_os = "macos")]
|
||||
SandboxType::MacosSeatbelt => {
|
||||
use crate::seatbelt::CreateSeatbeltCommandArgsParams;
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use crate::seatbelt::create_seatbelt_command_args;
|
||||
|
||||
let pending = pending_sandboxed_request?;
|
||||
let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
|
||||
command: os_argv_to_strings(argv),
|
||||
file_system_sandbox_policy: &effective_file_system_policy,
|
||||
network_sandbox_policy: effective_network_policy,
|
||||
sandbox_policy_cwd: native_sandbox_policy_cwd.as_path(),
|
||||
file_system_sandbox_policy: &pending.effective_file_system_policy,
|
||||
network_sandbox_policy: pending.effective_network_policy,
|
||||
sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(),
|
||||
enforce_managed_network,
|
||||
network,
|
||||
extra_allow_unix_sockets: &[],
|
||||
@@ -314,52 +350,84 @@ impl SandboxManager {
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
|
||||
full_command.append(&mut args);
|
||||
(full_command, None)
|
||||
(full_command, None, Some(pending))
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
|
||||
SandboxType::LinuxSeccomp => {
|
||||
let pending = pending_sandboxed_request?;
|
||||
let exe = codex_linux_sandbox_exe
|
||||
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
|
||||
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
|
||||
#[cfg(target_os = "linux")]
|
||||
ensure_linux_bubblewrap_is_supported(
|
||||
&effective_file_system_policy,
|
||||
&pending.effective_file_system_policy,
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
is_wsl1(),
|
||||
)?;
|
||||
let mut args = create_linux_sandbox_command_args_for_permission_profile(
|
||||
os_argv_to_strings(argv),
|
||||
native_command_cwd.as_path(),
|
||||
&effective_permission_profile,
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
pending.native_command_cwd.as_path(),
|
||||
&pending.effective_permission_profile,
|
||||
pending.native_sandbox_policy_cwd.as_path(),
|
||||
use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
|
||||
full_command.append(&mut args);
|
||||
(full_command, Some(linux_sandbox_arg0_override(exe)))
|
||||
(
|
||||
full_command,
|
||||
Some(linux_sandbox_arg0_override(exe)),
|
||||
Some(pending),
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
|
||||
SandboxType::WindowsRestrictedToken => (
|
||||
os_argv_to_strings(argv),
|
||||
None,
|
||||
Some(pending_sandboxed_request?),
|
||||
),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
|
||||
SandboxType::WindowsRestrictedToken => (
|
||||
os_argv_to_strings(argv),
|
||||
None,
|
||||
Some(pending_sandboxed_request?),
|
||||
),
|
||||
};
|
||||
|
||||
// Unsandboxed exec-server requests may have foreign cwd values that cannot be prepared
|
||||
// locally, but their effective permissions must still be preserved. In that case, carry
|
||||
// forward the base profile and its derived runtime policies.
|
||||
let (permission_profile, file_system_sandbox_policy, network_sandbox_policy) =
|
||||
pending_sandboxed_request.map_or(
|
||||
(
|
||||
base_effective_permission_profile,
|
||||
base_file_system_policy,
|
||||
base_network_policy,
|
||||
),
|
||||
|pending| {
|
||||
(
|
||||
pending.effective_permission_profile,
|
||||
pending.effective_file_system_policy,
|
||||
pending.effective_network_policy,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Ok(SandboxExecRequest {
|
||||
command: argv,
|
||||
cwd: native_command_cwd,
|
||||
sandbox_policy_cwd: native_sandbox_policy_cwd,
|
||||
cwd: command.cwd,
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.clone(),
|
||||
env: command.env,
|
||||
network: network.cloned(),
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
permission_profile: effective_permission_profile,
|
||||
file_system_sandbox_policy: effective_file_system_policy,
|
||||
network_sandbox_policy: effective_network_policy,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
arg0: arg0_override,
|
||||
})
|
||||
}
|
||||
@@ -406,6 +474,21 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
codex_home: &Path,
|
||||
) -> Result<(), SandboxTransformError> {
|
||||
// TODO(anp): Keep PathUri through the Windows sandbox wrapper boundary.
|
||||
let native_cwd =
|
||||
request
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|source| SandboxTransformError::InvalidCommandCwd {
|
||||
cwd: request.cwd.clone(),
|
||||
source,
|
||||
})?;
|
||||
let native_sandbox_policy_cwd = request.sandbox_policy_cwd.to_abs_path().map_err(|source| {
|
||||
SandboxTransformError::InvalidSandboxPolicyCwd {
|
||||
cwd: request.sandbox_policy_cwd.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let Some(program) = request.command.first_mut() else {
|
||||
return Err(SandboxTransformError::WindowsSandboxPreparation(
|
||||
"sandbox command was empty".to_string(),
|
||||
@@ -423,14 +506,14 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
resolve_windows_elevated_filesystem_overrides(
|
||||
request.sandbox,
|
||||
&request.permission_profile,
|
||||
&request.sandbox_policy_cwd,
|
||||
&native_sandbox_policy_cwd,
|
||||
use_elevated,
|
||||
)
|
||||
} else {
|
||||
resolve_windows_restricted_token_filesystem_overrides(
|
||||
request.sandbox,
|
||||
&request.permission_profile,
|
||||
&request.sandbox_policy_cwd,
|
||||
&native_sandbox_policy_cwd,
|
||||
request.windows_sandbox_level,
|
||||
)
|
||||
}
|
||||
@@ -454,7 +537,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
||||
let mut wrapper_args =
|
||||
codex_windows_sandbox::create_windows_sandbox_command_args_for_permission_profile(
|
||||
inner_command,
|
||||
&request.cwd,
|
||||
&native_cwd,
|
||||
workspace_roots,
|
||||
&request.env,
|
||||
&request.permission_profile,
|
||||
|
||||
@@ -74,10 +74,13 @@ fn restricted_file_system_uses_platform_sandbox_without_managed_network() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() {
|
||||
fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_policy() {
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||
let cwd_uri = if cfg!(windows) {
|
||||
PathUri::parse("file:///workspace/remote").expect("POSIX path URI")
|
||||
} else {
|
||||
PathUri::parse("file:///C:/workspace/remote").expect("Windows path URI")
|
||||
};
|
||||
let permissions = PermissionProfile::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
@@ -103,8 +106,8 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
})
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(exec_request.cwd, cwd);
|
||||
assert_eq!(exec_request.sandbox_policy_cwd, cwd);
|
||||
assert_eq!(exec_request.cwd, cwd_uri);
|
||||
assert_eq!(exec_request.sandbox_policy_cwd, cwd_uri);
|
||||
assert_eq!(
|
||||
exec_request.file_system_sandbox_policy,
|
||||
FileSystemSandboxPolicy::unrestricted()
|
||||
|
||||
@@ -33,6 +33,10 @@ use ts_rs::TS;
|
||||
pub struct LegacyAppPathString(String);
|
||||
|
||||
impl LegacyAppPathString {
|
||||
pub(super) fn from_str(path: &str) -> Self {
|
||||
Self(path.to_string())
|
||||
}
|
||||
|
||||
/// Renders an absolute path using the current host's path convention.
|
||||
pub fn from_abs_path(path: &AbsolutePathBuf) -> Self {
|
||||
Self(path.to_string_lossy().into_owned())
|
||||
|
||||
@@ -211,18 +211,20 @@ impl PathUri {
|
||||
Some(Self(url))
|
||||
}
|
||||
|
||||
/// Lexically joins a relative URI path onto this URI.
|
||||
/// Lexically resolves native absolute or relative path text against this URI.
|
||||
///
|
||||
/// Path text is interpreted using the POSIX or Windows convention inferred
|
||||
/// from the base URI. An absolute path replaces the base URI's path, while a
|
||||
/// relative path is appended lexically. Windows root-relative paths retain
|
||||
/// the base drive or UNC share, while drive-relative paths are rejected.
|
||||
/// Empty and `.` segments are ignored, while `..` removes one segment
|
||||
/// without escaping the URI root. Literal `%`, `?`, and `#` characters are
|
||||
/// percent-encoded as filename text. Paths containing a null character are
|
||||
/// rejected because they cannot be safely converted to native paths.
|
||||
/// without escaping the POSIX root, Windows drive, or UNC share. Literal
|
||||
/// `%`, `?`, and `#` characters are percent-encoded as filename text. Paths
|
||||
/// containing a null character are rejected because they cannot be safely
|
||||
/// converted to native paths.
|
||||
/// Opaque fallback URIs created by [`Self::from_abs_path`] reject non-empty
|
||||
/// joins.
|
||||
pub fn join(&self, path: &str) -> Result<Self, PathUriParseError> {
|
||||
if path.starts_with('/') {
|
||||
return Err(PathUriParseError::JoinPathMustBeRelative(path.to_string()));
|
||||
}
|
||||
if path.contains('\0') {
|
||||
return Err(PathUriParseError::InvalidFileUriPath {
|
||||
path: path.to_string(),
|
||||
@@ -231,6 +233,24 @@ impl PathUri {
|
||||
if path.is_empty() {
|
||||
return Ok(self.clone());
|
||||
}
|
||||
let convention =
|
||||
self.infer_path_convention()
|
||||
.ok_or_else(|| PathUriParseError::InvalidFileUriPath {
|
||||
path: self.to_string(),
|
||||
})?;
|
||||
// An absolute native path is already fully resolved, so replace the base URI's main path
|
||||
// instead of appending it.
|
||||
if let Ok(absolute) = LegacyAppPathString::from_str(path).to_path_uri(convention) {
|
||||
return Ok(absolute);
|
||||
}
|
||||
let path_bytes = path.as_bytes();
|
||||
if convention == PathConvention::Windows
|
||||
&& matches!(path_bytes, [drive, b':', ..] if drive.is_ascii_alphabetic())
|
||||
{
|
||||
return Err(PathUriParseError::InvalidFileUriPath {
|
||||
path: path.to_string(),
|
||||
});
|
||||
}
|
||||
if decode_bad_path_uri(&self.0).is_some() {
|
||||
return Err(PathUriParseError::InvalidFileUriPath {
|
||||
path: self.to_string(),
|
||||
@@ -238,19 +258,40 @@ impl PathUri {
|
||||
}
|
||||
|
||||
let mut url = self.0.clone();
|
||||
let anchor_depth = usize::from(convention == PathConvention::Windows);
|
||||
let mut depth = url
|
||||
.path_segments()
|
||||
.map(|segments| segments.filter(|segment| !segment.is_empty()).count())
|
||||
.unwrap_or_default();
|
||||
let windows_root_relative = convention == PathConvention::Windows
|
||||
&& matches!(path_bytes, [b'\\' | b'/', rest @ ..] if !matches!(rest, [b'\\' | b'/', ..]));
|
||||
{
|
||||
let Ok(mut segments) = url.path_segments_mut() else {
|
||||
unreachable!("validated file URLs support hierarchical path segments");
|
||||
};
|
||||
segments.pop_if_empty();
|
||||
if windows_root_relative {
|
||||
while depth > anchor_depth {
|
||||
segments.pop();
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
let path = match convention {
|
||||
PathConvention::Posix => path.to_string(),
|
||||
PathConvention::Windows => path.replace('\\', "/"),
|
||||
};
|
||||
for component in path.split('/') {
|
||||
match component {
|
||||
"" | "." => {}
|
||||
".." => {
|
||||
segments.pop();
|
||||
if depth > anchor_depth {
|
||||
segments.pop();
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
component => {
|
||||
segments.push(component);
|
||||
depth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -350,6 +391,12 @@ impl TryFrom<String> for PathUri {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AbsolutePathBuf> for PathUri {
|
||||
fn from(p: AbsolutePathBuf) -> Self {
|
||||
Self::from_abs_path(&p)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PathUri {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@@ -522,13 +522,72 @@ fn join_normalizes_relative_uri_segments() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_rejects_absolute_and_null_paths() {
|
||||
fn join_replaces_posix_absolute_path() {
|
||||
let base = PathUri::parse("file:///workspace").expect("valid base URI");
|
||||
|
||||
assert!(matches!(
|
||||
assert_eq!(
|
||||
base.join("/src"),
|
||||
Err(PathUriParseError::JoinPathMustBeRelative(path)) if path == "/src"
|
||||
));
|
||||
Ok(PathUri::parse("file:///src").expect("valid absolute URI"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_replaces_windows_absolute_path() {
|
||||
let base = PathUri::parse("file:///C:/workspace/src").expect("valid base URI");
|
||||
|
||||
assert_eq!(
|
||||
base.join(r"D:\tmp\test.rs"),
|
||||
Ok(PathUri::parse("file:///D:/tmp/test.rs").expect("valid absolute URI"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_windows_root_relative_path_preserves_drive_or_share() {
|
||||
for (base, path, expected) in [
|
||||
("file:///C:/base/dir", r"\Windows", "file:///C:/Windows"),
|
||||
(
|
||||
"file://server/share/base/dir",
|
||||
r"\Windows",
|
||||
"file://server/share/Windows",
|
||||
),
|
||||
] {
|
||||
let base = PathUri::parse(base).expect("valid base URI");
|
||||
let expected = PathUri::parse(expected).expect("valid expected URI");
|
||||
assert_eq!(base.join(path), Ok(expected), "joining {path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_rejects_windows_drive_relative_path() {
|
||||
let base = PathUri::parse("file:///C:/base").expect("valid base URI");
|
||||
|
||||
assert_eq!(
|
||||
base.join(r"D:tmp"),
|
||||
Err(PathUriParseError::InvalidFileUriPath {
|
||||
path: r"D:tmp".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_parent_segments_preserve_windows_drive_or_share_anchor() {
|
||||
for (base, expected) in [
|
||||
("file:///C:/base/dir", "file:///C:/Windows"),
|
||||
(
|
||||
"file://server/share/base/dir",
|
||||
"file://server/share/Windows",
|
||||
),
|
||||
] {
|
||||
let base = PathUri::parse(base).expect("valid base URI");
|
||||
let expected = PathUri::parse(expected).expect("valid expected URI");
|
||||
assert_eq!(base.join(r"..\..\..\Windows"), Ok(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_rejects_null_paths() {
|
||||
let base = PathUri::parse("file:///workspace").expect("valid base URI");
|
||||
|
||||
assert_eq!(
|
||||
base.join("src\0file"),
|
||||
Err(PathUriParseError::InvalidFileUriPath {
|
||||
@@ -537,6 +596,26 @@ fn join_rejects_absolute_and_null_paths() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_uses_the_base_uri_path_convention() {
|
||||
for (base, path, expected) in [
|
||||
(
|
||||
"file:///workspace/src",
|
||||
"../tests/test.rs",
|
||||
"file:///workspace/tests/test.rs",
|
||||
),
|
||||
(
|
||||
"file:///C:/workspace/src",
|
||||
r"..\tests\test.rs",
|
||||
"file:///C:/workspace/tests/test.rs",
|
||||
),
|
||||
] {
|
||||
let base = PathUri::parse(base).expect("valid base URI");
|
||||
let expected = PathUri::parse(expected).expect("valid expected URI");
|
||||
assert_eq!(base.join(path), Ok(expected), "joining {path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_url_returns_the_validated_url() {
|
||||
let uri = PathUri::parse("file://localhost/workspace/a%20file.rs").expect("valid file URI");
|
||||
|
||||
Reference in New Issue
Block a user