mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
windows-sandbox: send permission profiles to elevated runner (#22918)
## Why This is the next PR in the Windows sandbox migration stack after #22896. The bottom PR introduces a Windows-local resolved permissions helper while existing callers still start from legacy `SandboxPolicy`. This PR moves the elevated runner IPC boundary to `PermissionProfile`, which makes the direction of the stack visible without changing the public core call sites yet. Because that changes the CLI-to-command-runner message shape, the framed IPC protocol version is bumped in the same PR so the boundary change is explicit. ## What changed - Replaced elevated IPC `policy_json_or_preset`/`sandbox_policy_cwd` fields with `permission_profile`/`permission_profile_cwd`. - Bumped the elevated command-runner IPC protocol to `IPC_PROTOCOL_VERSION = 2` and switched parent/runner frames to use the shared constant. - Converted the parent elevated paths from the parsed legacy policy into a materialized `PermissionProfile` before sending the runner request. - Added `WindowsSandboxTokenMode` resolution for managed `PermissionProfile` values and made the runner choose read-only vs writable-root capability tokens from that resolved profile. - Rejected disabled, external, unrestricted, and full-disk-write profiles before token selection. - Added IPC JSON coverage for tagged `PermissionProfile` payloads and token-mode unit coverage for the resolved permission helper. ## Verification - `cargo test -p codex-windows-sandbox` - `just fix -p codex-windows-sandbox` - `cargo check -p codex-windows-sandbox --target x86_64-pc-windows-msvc --tests` was attempted locally but blocked before crate type-checking because the macOS compiler environment lacks Windows C headers such as `windows.h` and `assert.h`; GitHub Windows CI is the required verification for the runner path. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22918). * #23715 * #23714 * #23167 * #22923 * __->__ #22918
This commit is contained in:
1 parent
cb05de6724
commit
729bdf3c8d
8 files changed
+273
-27
No files matched your search
@@ -16,17 +16,18 @@ use anyhow::Result;
|
|||||||
use codex_windows_sandbox::ErrorPayload;
|
use codex_windows_sandbox::ErrorPayload;
|
||||||
use codex_windows_sandbox::ExitPayload;
|
use codex_windows_sandbox::ExitPayload;
|
||||||
use codex_windows_sandbox::FramedMessage;
|
use codex_windows_sandbox::FramedMessage;
|
||||||
|
use codex_windows_sandbox::IPC_PROTOCOL_VERSION;
|
||||||
use codex_windows_sandbox::LocalSid;
|
use codex_windows_sandbox::LocalSid;
|
||||||
use codex_windows_sandbox::Message;
|
use codex_windows_sandbox::Message;
|
||||||
use codex_windows_sandbox::OutputPayload;
|
use codex_windows_sandbox::OutputPayload;
|
||||||
use codex_windows_sandbox::OutputStream;
|
use codex_windows_sandbox::OutputStream;
|
||||||
use codex_windows_sandbox::PipeSpawnHandles;
|
use codex_windows_sandbox::PipeSpawnHandles;
|
||||||
use codex_windows_sandbox::ResizePayload;
|
use codex_windows_sandbox::ResizePayload;
|
||||||
use codex_windows_sandbox::SandboxPolicy;
|
|
||||||
use codex_windows_sandbox::SpawnReady;
|
use codex_windows_sandbox::SpawnReady;
|
||||||
use codex_windows_sandbox::SpawnRequest;
|
use codex_windows_sandbox::SpawnRequest;
|
||||||
use codex_windows_sandbox::StderrMode;
|
use codex_windows_sandbox::StderrMode;
|
||||||
use codex_windows_sandbox::StdinMode;
|
use codex_windows_sandbox::StdinMode;
|
||||||
|
use codex_windows_sandbox::WindowsSandboxTokenMode;
|
||||||
use codex_windows_sandbox::allow_null_device;
|
use codex_windows_sandbox::allow_null_device;
|
||||||
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
|
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
|
||||||
use codex_windows_sandbox::create_workspace_write_token_with_caps_and_user_from;
|
use codex_windows_sandbox::create_workspace_write_token_with_caps_and_user_from;
|
||||||
@@ -35,11 +36,11 @@ use codex_windows_sandbox::encode_bytes;
|
|||||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||||
use codex_windows_sandbox::hide_current_user_profile_dir;
|
use codex_windows_sandbox::hide_current_user_profile_dir;
|
||||||
use codex_windows_sandbox::log_note;
|
use codex_windows_sandbox::log_note;
|
||||||
use codex_windows_sandbox::parse_policy;
|
|
||||||
use codex_windows_sandbox::read_frame;
|
use codex_windows_sandbox::read_frame;
|
||||||
use codex_windows_sandbox::read_handle_loop;
|
use codex_windows_sandbox::read_handle_loop;
|
||||||
use codex_windows_sandbox::spawn_process_with_pipes;
|
use codex_windows_sandbox::spawn_process_with_pipes;
|
||||||
use codex_windows_sandbox::to_wide;
|
use codex_windows_sandbox::to_wide;
|
||||||
|
use codex_windows_sandbox::token_mode_for_permission_profile;
|
||||||
use codex_windows_sandbox::write_frame;
|
use codex_windows_sandbox::write_frame;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -167,7 +168,7 @@ fn open_pipe(name: &str, access: u32) -> Result<HANDLE> {
|
|||||||
/// Send an error frame back to the parent process.
|
/// Send an error frame back to the parent process.
|
||||||
fn send_error(writer: &Arc<StdMutex<File>>, code: &str, message: String) -> Result<()> {
|
fn send_error(writer: &Arc<StdMutex<File>>, code: &str, message: String) -> Result<()> {
|
||||||
let msg = FramedMessage {
|
let msg = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Error {
|
message: Message::Error {
|
||||||
payload: ErrorPayload {
|
payload: ErrorPayload {
|
||||||
message,
|
message,
|
||||||
@@ -186,7 +187,7 @@ fn read_spawn_request(reader: &mut File) -> Result<SpawnRequest> {
|
|||||||
let Some(msg) = read_frame(reader)? else {
|
let Some(msg) = read_frame(reader)? else {
|
||||||
anyhow::bail!("runner: pipe closed before spawn_request");
|
anyhow::bail!("runner: pipe closed before spawn_request");
|
||||||
};
|
};
|
||||||
if msg.version != 1 {
|
if msg.version != IPC_PROTOCOL_VERSION {
|
||||||
anyhow::bail!("runner: unsupported protocol version {}", msg.version);
|
anyhow::bail!("runner: unsupported protocol version {}", msg.version);
|
||||||
}
|
}
|
||||||
match msg.message {
|
match msg.message {
|
||||||
@@ -235,7 +236,12 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
|
|||||||
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
|
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
|
||||||
let log_dir = req.codex_home.clone();
|
let log_dir = req.codex_home.clone();
|
||||||
hide_current_user_profile_dir(req.codex_home.as_path());
|
hide_current_user_profile_dir(req.codex_home.as_path());
|
||||||
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
|
let token_mode = token_mode_for_permission_profile(
|
||||||
|
&req.permission_profile,
|
||||||
|
&req.permission_profile_cwd,
|
||||||
|
&req.env,
|
||||||
|
)
|
||||||
|
.context("resolve permission profile token mode")?;
|
||||||
let mut cap_psids: Vec<LocalSid> = Vec::new();
|
let mut cap_psids: Vec<LocalSid> = Vec::new();
|
||||||
for sid in &req.cap_sids {
|
for sid in &req.cap_sids {
|
||||||
cap_psids.push(
|
cap_psids.push(
|
||||||
@@ -253,16 +259,13 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
|
|||||||
let cap_psid_ptrs: Vec<*mut _> = cap_psids.iter().map(LocalSid::as_ptr).collect();
|
let cap_psid_ptrs: Vec<*mut _> = cap_psids.iter().map(LocalSid::as_ptr).collect();
|
||||||
let base = OwnedWinHandle::new(unsafe { get_current_token_for_restriction()? });
|
let base = OwnedWinHandle::new(unsafe { get_current_token_for_restriction()? });
|
||||||
let h_token = OwnedWinHandle::new(unsafe {
|
let h_token = OwnedWinHandle::new(unsafe {
|
||||||
match &policy {
|
match token_mode {
|
||||||
SandboxPolicy::ReadOnly { .. } => {
|
WindowsSandboxTokenMode::ReadOnlyCapability => {
|
||||||
create_readonly_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
create_readonly_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
||||||
}
|
}
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
WindowsSandboxTokenMode::WritableRootsCapability => {
|
||||||
create_workspace_write_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
create_workspace_write_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
||||||
}
|
}
|
||||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}?);
|
}?);
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -352,7 +355,7 @@ fn spawn_output_reader(
|
|||||||
) -> std::thread::JoinHandle<()> {
|
) -> std::thread::JoinHandle<()> {
|
||||||
read_handle_loop(handle, move |chunk| {
|
read_handle_loop(handle, move |chunk| {
|
||||||
let msg = FramedMessage {
|
let msg = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Output {
|
message: Message::Output {
|
||||||
payload: OutputPayload {
|
payload: OutputPayload {
|
||||||
data_b64: encode_bytes(chunk),
|
data_b64: encode_bytes(chunk),
|
||||||
@@ -554,7 +557,7 @@ pub fn main() -> Result<()> {
|
|||||||
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
|
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
|
||||||
|
|
||||||
let msg = FramedMessage {
|
let msg = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::SpawnReady {
|
message: Message::SpawnReady {
|
||||||
payload: SpawnReady {
|
payload: SpawnReady {
|
||||||
process_id: unsafe { GetProcessId(pi.hProcess) },
|
process_id: unsafe { GetProcessId(pi.hProcess) },
|
||||||
@@ -631,7 +634,7 @@ pub fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let exit_msg = FramedMessage {
|
let exit_msg = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Exit {
|
message: Message::Exit {
|
||||||
payload: ExitPayload {
|
payload: ExitPayload {
|
||||||
exit_code,
|
exit_code,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -23,6 +24,9 @@ use std::path::PathBuf;
|
|||||||
/// obviously invalid frames.
|
/// obviously invalid frames.
|
||||||
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
|
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Protocol version shared by the parent process and elevated command runner.
|
||||||
|
pub const IPC_PROTOCOL_VERSION: u8 = 2;
|
||||||
|
|
||||||
/// Length-prefixed, JSON-encoded frame.
|
/// Length-prefixed, JSON-encoded frame.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct FramedMessage {
|
pub struct FramedMessage {
|
||||||
@@ -55,8 +59,8 @@ pub struct SpawnRequest {
|
|||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
pub cwd: PathBuf,
|
pub cwd: PathBuf,
|
||||||
pub env: HashMap<String, String>,
|
pub env: HashMap<String, String>,
|
||||||
pub policy_json_or_preset: String,
|
pub permission_profile: PermissionProfile,
|
||||||
pub sandbox_policy_cwd: PathBuf,
|
pub permission_profile_cwd: PathBuf,
|
||||||
pub codex_home: PathBuf,
|
pub codex_home: PathBuf,
|
||||||
pub real_codex_home: PathBuf,
|
pub real_codex_home: PathBuf,
|
||||||
pub cap_sids: Vec<String>,
|
pub cap_sids: Vec<String>,
|
||||||
@@ -164,11 +168,12 @@ pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn framed_round_trip() {
|
fn framed_round_trip() {
|
||||||
let msg = FramedMessage {
|
let msg = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Output {
|
message: Message::Output {
|
||||||
payload: OutputPayload {
|
payload: OutputPayload {
|
||||||
data_b64: encode_bytes(b"hello"),
|
data_b64: encode_bytes(b"hello"),
|
||||||
@@ -179,7 +184,7 @@ mod tests {
|
|||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
write_frame(&mut buf, &msg).expect("write");
|
write_frame(&mut buf, &msg).expect("write");
|
||||||
let decoded = read_frame(buf.as_slice()).expect("read").expect("some");
|
let decoded = read_frame(buf.as_slice()).expect("read").expect("some");
|
||||||
assert_eq!(decoded.version, 1);
|
assert_eq!(decoded.version, IPC_PROTOCOL_VERSION);
|
||||||
match decoded.message {
|
match decoded.message {
|
||||||
Message::Output { payload } => {
|
Message::Output { payload } => {
|
||||||
assert_eq!(payload.stream, OutputStream::Stdout);
|
assert_eq!(payload.stream, OutputStream::Stdout);
|
||||||
@@ -189,4 +194,43 @@ mod tests {
|
|||||||
other => panic!("unexpected message: {other:?}"),
|
other => panic!("unexpected message: {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_request_serializes_permission_profile() {
|
||||||
|
let msg = FramedMessage {
|
||||||
|
version: IPC_PROTOCOL_VERSION,
|
||||||
|
message: Message::SpawnRequest {
|
||||||
|
payload: Box::new(SpawnRequest {
|
||||||
|
command: vec!["cmd.exe".to_string(), "/c".to_string(), "ver".to_string()],
|
||||||
|
cwd: PathBuf::from(r"C:\workspace"),
|
||||||
|
env: HashMap::new(),
|
||||||
|
permission_profile: PermissionProfile::read_only(),
|
||||||
|
permission_profile_cwd: PathBuf::from(r"C:\workspace"),
|
||||||
|
codex_home: PathBuf::from(r"C:\codex"),
|
||||||
|
real_codex_home: PathBuf::from(r"C:\Users\codex"),
|
||||||
|
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
|
||||||
|
timeout_ms: Some(1000),
|
||||||
|
tty: false,
|
||||||
|
stdin_open: false,
|
||||||
|
use_private_desktop: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = serde_json::to_value(&msg).expect("serialize");
|
||||||
|
assert_eq!("spawn_request", encoded["type"]);
|
||||||
|
assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]);
|
||||||
|
assert_eq!(None, encoded["payload"].get("policy_json_or_preset"));
|
||||||
|
assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd"));
|
||||||
|
|
||||||
|
let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize");
|
||||||
|
let Message::SpawnRequest { payload } = decoded.message else {
|
||||||
|
panic!("unexpected message");
|
||||||
|
};
|
||||||
|
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
|
||||||
|
assert_eq!(
|
||||||
|
PathBuf::from(r"C:\workspace"),
|
||||||
|
payload.permission_profile_cwd
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::identity::SandboxCreds;
|
use crate::identity::SandboxCreds;
|
||||||
use crate::ipc_framed::FramedMessage;
|
use crate::ipc_framed::FramedMessage;
|
||||||
|
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
|
||||||
use crate::ipc_framed::Message;
|
use crate::ipc_framed::Message;
|
||||||
use crate::ipc_framed::SpawnRequest;
|
use crate::ipc_framed::SpawnRequest;
|
||||||
use crate::ipc_framed::read_frame;
|
use crate::ipc_framed::read_frame;
|
||||||
@@ -56,7 +57,7 @@ pub(crate) struct RunnerTransport {
|
|||||||
impl RunnerTransport {
|
impl RunnerTransport {
|
||||||
pub(crate) fn send_spawn_request(&mut self, request: SpawnRequest) -> Result<()> {
|
pub(crate) fn send_spawn_request(&mut self, request: SpawnRequest) -> Result<()> {
|
||||||
let spawn_request = FramedMessage {
|
let spawn_request = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::SpawnRequest {
|
message: Message::SpawnRequest {
|
||||||
payload: Box::new(request),
|
payload: Box::new(request),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ mod windows_impl {
|
|||||||
use crate::setup::effective_write_roots_for_setup;
|
use crate::setup::effective_write_roots_for_setup;
|
||||||
use crate::token::LocalSid;
|
use crate::token::LocalSid;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -144,12 +145,14 @@ mod windows_impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(|| -> Result<CaptureResult> {
|
(|| -> Result<CaptureResult> {
|
||||||
|
let permission_profile =
|
||||||
|
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
|
||||||
let spawn_request = SpawnRequest {
|
let spawn_request = SpawnRequest {
|
||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
cwd: cwd.to_path_buf(),
|
cwd: cwd.to_path_buf(),
|
||||||
env: env_map.clone(),
|
env: env_map.clone(),
|
||||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
permission_profile,
|
||||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||||
codex_home: sandbox_base.clone(),
|
codex_home: sandbox_base.clone(),
|
||||||
real_codex_home: codex_home.to_path_buf(),
|
real_codex_home: codex_home.to_path_buf(),
|
||||||
cap_sids,
|
cap_sids,
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ pub use ipc_framed::ExitPayload;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use ipc_framed::FramedMessage;
|
pub use ipc_framed::FramedMessage;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use ipc_framed::IPC_PROTOCOL_VERSION;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub use ipc_framed::Message;
|
pub use ipc_framed::Message;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use ipc_framed::OutputPayload;
|
pub use ipc_framed::OutputPayload;
|
||||||
@@ -197,6 +199,10 @@ pub use process::read_handle_loop;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use process::spawn_process_with_pipes;
|
pub use process::spawn_process_with_pipes;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use resolved_permissions::WindowsSandboxTokenMode;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use resolved_permissions::token_mode_for_permission_profile;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub use setup::SETUP_VERSION;
|
pub use setup::SETUP_VERSION;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use setup::SandboxSetupRequest;
|
pub use setup::SandboxSetupRequest;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_protocol::permissions::FileSystemPath;
|
use codex_protocol::permissions::FileSystemPath;
|
||||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||||
|
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_protocol::protocol::SandboxPolicy;
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
@@ -25,6 +28,33 @@ pub(crate) struct WindowsWritableRoot {
|
|||||||
pub(crate) read_only_subpaths: Vec<PathBuf>,
|
pub(crate) read_only_subpaths: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restricted-token family needed to enforce a Windows permission profile.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WindowsSandboxTokenMode {
|
||||||
|
ReadOnlyCapability,
|
||||||
|
WritableRootsCapability,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses the restricted-token family needed for a managed permission profile.
|
||||||
|
pub fn token_mode_for_permission_profile(
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
cwd: &Path,
|
||||||
|
env_map: &HashMap<String, String>,
|
||||||
|
) -> Result<WindowsSandboxTokenMode> {
|
||||||
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::try_from_permission_profile(permission_profile)?;
|
||||||
|
if permissions.file_system.has_full_disk_write_access() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"permission profile requests full-disk filesystem writes, which cannot be enforced by the Windows sandbox"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if permissions.writable_roots_for_cwd(cwd, env_map).is_empty() {
|
||||||
|
Ok(WindowsSandboxTokenMode::ReadOnlyCapability)
|
||||||
|
} else {
|
||||||
|
Ok(WindowsSandboxTokenMode::WritableRootsCapability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ResolvedWindowsSandboxPermissions {
|
impl ResolvedWindowsSandboxPermissions {
|
||||||
pub(crate) fn from_legacy_policy(policy: &SandboxPolicy) -> Self {
|
pub(crate) fn from_legacy_policy(policy: &SandboxPolicy) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -40,6 +70,26 @@ impl ResolvedWindowsSandboxPermissions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_from_permission_profile(
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
) -> Result<Self> {
|
||||||
|
if !matches!(permission_profile, PermissionProfile::Managed { .. }) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"only managed permission profiles can be enforced by the Windows sandbox"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let (file_system, network) = permission_profile.to_runtime_permissions();
|
||||||
|
if !matches!(file_system.kind, FileSystemSandboxKind::Restricted) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"only restricted managed filesystem permissions can be enforced by the Windows sandbox"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
file_system,
|
||||||
|
network,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn should_apply_network_block(&self) -> bool {
|
pub(crate) fn should_apply_network_block(&self) -> bool {
|
||||||
!self.network.is_enabled()
|
!self.network.is_enabled()
|
||||||
}
|
}
|
||||||
@@ -114,3 +164,135 @@ fn windows_temp_env_roots(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
|
|||||||
.filter(|path| path.is_absolute())
|
.filter(|path| path.is_absolute())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||||
|
use codex_protocol::permissions::FileSystemAccessMode;
|
||||||
|
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||||
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn permission_profile_workspace_write_uses_windows_temp_env_vars() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let cwd = tmp.path().join("workspace");
|
||||||
|
let temp_dir = tmp.path().join("temp");
|
||||||
|
std::fs::create_dir_all(&cwd).expect("create cwd");
|
||||||
|
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
|
||||||
|
|
||||||
|
let mut env_map = HashMap::new();
|
||||||
|
env_map.insert("TEMP".to_string(), temp_dir.to_string_lossy().to_string());
|
||||||
|
env_map.insert("TMP".to_string(), temp_dir.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
|
||||||
|
&PermissionProfile::workspace_write(),
|
||||||
|
)
|
||||||
|
.expect("managed permission profile");
|
||||||
|
let roots = permissions
|
||||||
|
.writable_roots_for_cwd(&cwd, &env_map)
|
||||||
|
.into_iter()
|
||||||
|
.map(|root| root.root)
|
||||||
|
.collect::<std::collections::HashSet<_>>();
|
||||||
|
|
||||||
|
let expected_roots = [
|
||||||
|
temp_dir,
|
||||||
|
dunce::canonicalize(&cwd).expect("canonicalize cwd"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.collect::<std::collections::HashSet<_>>();
|
||||||
|
|
||||||
|
assert_eq!(expected_roots, roots);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let cwd = tmp.path().join("workspace");
|
||||||
|
std::fs::create_dir_all(&cwd).expect("create cwd");
|
||||||
|
|
||||||
|
let token_mode = token_mode_for_permission_profile(
|
||||||
|
&PermissionProfile::read_only(),
|
||||||
|
&cwd,
|
||||||
|
&HashMap::new(),
|
||||||
|
)
|
||||||
|
.expect("token mode");
|
||||||
|
|
||||||
|
assert_eq!(WindowsSandboxTokenMode::ReadOnlyCapability, token_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_mode_for_profile_with_writable_roots_uses_write_capabilities() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let cwd = tmp.path().join("workspace");
|
||||||
|
std::fs::create_dir_all(&cwd).expect("create cwd");
|
||||||
|
|
||||||
|
let token_mode = token_mode_for_permission_profile(
|
||||||
|
&PermissionProfile::workspace_write(),
|
||||||
|
&cwd,
|
||||||
|
&HashMap::new(),
|
||||||
|
)
|
||||||
|
.expect("token mode");
|
||||||
|
|
||||||
|
assert_eq!(WindowsSandboxTokenMode::WritableRootsCapability, token_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn permission_profile_rejects_disabled_profiles() {
|
||||||
|
let err = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
|
||||||
|
&PermissionProfile::Disabled,
|
||||||
|
)
|
||||||
|
.expect_err("disabled profile should not resolve for sandbox enforcement");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("only managed permission profiles can be enforced")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn permission_profile_rejects_unrestricted_managed_filesystem() {
|
||||||
|
let permission_profile = PermissionProfile::Managed {
|
||||||
|
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||||
|
network: NetworkSandboxPolicy::Restricted,
|
||||||
|
};
|
||||||
|
|
||||||
|
let err =
|
||||||
|
ResolvedWindowsSandboxPermissions::try_from_permission_profile(&permission_profile)
|
||||||
|
.expect_err("unrestricted profile should not resolve for sandbox enforcement");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("only restricted managed filesystem permissions can be enforced")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_mode_rejects_full_disk_write_entries() {
|
||||||
|
let tmp = TempDir::new().expect("tempdir");
|
||||||
|
let cwd = tmp.path().join("workspace");
|
||||||
|
std::fs::create_dir_all(&cwd).expect("create cwd");
|
||||||
|
let permission_profile = PermissionProfile::Managed {
|
||||||
|
file_system: ManagedFileSystemPermissions::Restricted {
|
||||||
|
entries: vec![FileSystemSandboxEntry {
|
||||||
|
path: FileSystemPath::Special {
|
||||||
|
value: FileSystemSpecialPath::Root,
|
||||||
|
},
|
||||||
|
access: FileSystemAccessMode::Write,
|
||||||
|
}],
|
||||||
|
glob_scan_max_depth: None,
|
||||||
|
},
|
||||||
|
network: NetworkSandboxPolicy::Restricted,
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = token_mode_for_permission_profile(&permission_profile, &cwd, &HashMap::new())
|
||||||
|
.expect_err("full disk writes should not resolve to a token mode");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("full-disk filesystem writes, which cannot be enforced")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,13 @@ use super::windows_common::start_runner_stdin_writer;
|
|||||||
use super::windows_common::start_runner_stdout_reader;
|
use super::windows_common::start_runner_stdout_reader;
|
||||||
use crate::ipc_framed::EmptyPayload;
|
use crate::ipc_framed::EmptyPayload;
|
||||||
use crate::ipc_framed::FramedMessage;
|
use crate::ipc_framed::FramedMessage;
|
||||||
|
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
|
||||||
use crate::ipc_framed::Message;
|
use crate::ipc_framed::Message;
|
||||||
use crate::ipc_framed::SpawnRequest;
|
use crate::ipc_framed::SpawnRequest;
|
||||||
use crate::runner_client::spawn_runner_transport;
|
use crate::runner_client::spawn_runner_transport;
|
||||||
use crate::spawn_prep::prepare_elevated_spawn_context;
|
use crate::spawn_prep::prepare_elevated_spawn_context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_pty::ProcessDriver;
|
use codex_utils_pty::ProcessDriver;
|
||||||
use codex_utils_pty::SpawnedProcess;
|
use codex_utils_pty::SpawnedProcess;
|
||||||
@@ -60,12 +62,16 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
|||||||
&deny_write_paths_override,
|
&deny_write_paths_override,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||||
|
&elevated.common.policy,
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
);
|
||||||
let spawn_request = SpawnRequest {
|
let spawn_request = SpawnRequest {
|
||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
cwd: cwd.to_path_buf(),
|
cwd: cwd.to_path_buf(),
|
||||||
env: env_map.clone(),
|
env: env_map.clone(),
|
||||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
permission_profile,
|
||||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||||
codex_home: elevated.common.sandbox_base.clone(),
|
codex_home: elevated.common.sandbox_base.clone(),
|
||||||
real_codex_home: codex_home.to_path_buf(),
|
real_codex_home: codex_home.to_path_buf(),
|
||||||
cap_sids: elevated.cap_sids.clone(),
|
cap_sids: elevated.cap_sids.clone(),
|
||||||
@@ -106,7 +112,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
|||||||
let outbound_tx = outbound_tx.clone();
|
let outbound_tx = outbound_tx.clone();
|
||||||
Some(Box::new(move || {
|
Some(Box::new(move || {
|
||||||
let _ = outbound_tx.send(FramedMessage {
|
let _ = outbound_tx.send(FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Terminate {
|
message: Message::Terminate {
|
||||||
payload: EmptyPayload::default(),
|
payload: EmptyPayload::default(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::ipc_framed::EmptyPayload;
|
use crate::ipc_framed::EmptyPayload;
|
||||||
use crate::ipc_framed::FramedMessage;
|
use crate::ipc_framed::FramedMessage;
|
||||||
|
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
|
||||||
use crate::ipc_framed::Message;
|
use crate::ipc_framed::Message;
|
||||||
use crate::ipc_framed::OutputStream;
|
use crate::ipc_framed::OutputStream;
|
||||||
use crate::ipc_framed::ResizePayload;
|
use crate::ipc_framed::ResizePayload;
|
||||||
@@ -70,7 +71,7 @@ pub(crate) fn start_runner_stdin_writer(
|
|||||||
bytes
|
bytes
|
||||||
};
|
};
|
||||||
let msg = FramedMessage {
|
let msg = FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Stdin {
|
message: Message::Stdin {
|
||||||
payload: StdinPayload {
|
payload: StdinPayload {
|
||||||
data_b64: encode_bytes(&bytes),
|
data_b64: encode_bytes(&bytes),
|
||||||
@@ -83,7 +84,7 @@ pub(crate) fn start_runner_stdin_writer(
|
|||||||
}
|
}
|
||||||
if stdin_open {
|
if stdin_open {
|
||||||
let _ = outbound_tx.send(FramedMessage {
|
let _ = outbound_tx.send(FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::CloseStdin {
|
message: Message::CloseStdin {
|
||||||
payload: EmptyPayload::default(),
|
payload: EmptyPayload::default(),
|
||||||
},
|
},
|
||||||
@@ -165,7 +166,7 @@ pub(crate) fn make_runner_resizer(
|
|||||||
Box::new(move |size: TerminalSize| {
|
Box::new(move |size: TerminalSize| {
|
||||||
outbound_tx
|
outbound_tx
|
||||||
.send(FramedMessage {
|
.send(FramedMessage {
|
||||||
version: 1,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Resize {
|
message: Message::Resize {
|
||||||
payload: ResizePayload {
|
payload: ResizePayload {
|
||||||
rows: size.rows,
|
rows: size.rows,
|
||||||
|
|||||||
Reference in new issue
Block a user