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:
Michael Bolin authored and GitHub committed 2026-05-20 12:41:06 -07:00
1 parent cb05de6724
commit 729bdf3c8d
8 files changed
+273 -27

No files matched your search

@@ -10,6 +10,7 @@
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_protocol::models::PermissionProfile;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -23,6 +24,9 @@ use std::path::PathBuf;
/// obviously invalid frames.
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.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FramedMessage {
@@ -55,8 +59,8 @@ pub struct SpawnRequest {
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub policy_json_or_preset: String,
pub sandbox_policy_cwd: PathBuf,
pub permission_profile: PermissionProfile,
pub permission_profile_cwd: PathBuf,
pub codex_home: PathBuf,
pub real_codex_home: PathBuf,
pub cap_sids: Vec<String>,
@@ -164,11 +168,12 @@ pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn framed_round_trip() {
let msg = FramedMessage {
version: 1,
version: IPC_PROTOCOL_VERSION,
message: Message::Output {
payload: OutputPayload {
data_b64: encode_bytes(b"hello"),
@@ -179,7 +184,7 @@ mod tests {
let mut buf = Vec::new();
write_frame(&mut buf, &msg).expect("write");
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 {
Message::Output { payload } => {
assert_eq!(payload.stream, OutputStream::Stdout);
@@ -189,4 +194,43 @@ mod tests {
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::ipc_framed::FramedMessage;
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::ipc_framed::read_frame;
@@ -56,7 +57,7 @@ pub(crate) struct RunnerTransport {
impl RunnerTransport {
pub(crate) fn send_spawn_request(&mut self, request: SpawnRequest) -> Result<()> {
let spawn_request = FramedMessage {
version: 1,
version: IPC_PROTOCOL_VERSION,
message: Message::SpawnRequest {
payload: Box::new(request),
},