Merge 5032154ecf into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-02-12 14:15:59 -08:00
committed by GitHub
29 changed files with 363 additions and 46 deletions
@@ -88,7 +88,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -104,7 +104,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -241,7 +241,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -208,7 +208,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -12,7 +12,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -104,7 +104,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -16,7 +16,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -12,7 +12,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -12,7 +12,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -104,7 +104,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -16,7 +16,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
@@ -104,7 +104,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
+1 -1
View File
@@ -100,7 +100,7 @@
"type": "string"
},
{
"description": "*All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox.",
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
"enum": [
"on-failure"
],
+44 -2
View File
@@ -81,7 +81,6 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
resets_at,
rate_limits: rate_limits.map(Box::new),
promo_message,
limit_name: limit_id,
});
} else if err.error.error_type.as_deref() == Some("usage_not_included") {
return CodexErr::UsageNotIncluded;
@@ -156,6 +155,10 @@ mod tests {
ACTIVE_LIMIT_HEADER,
http::HeaderValue::from_static("codex_other"),
);
headers.insert(
"x-codex-other-limit-name",
http::HeaderValue::from_static("codex_other"),
);
let body = serde_json::json!({
"error": {
"type": "usage_limit_reached",
@@ -173,7 +176,46 @@ mod tests {
let CodexErr::UsageLimitReached(usage_limit) = err else {
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
};
assert_eq!(usage_limit.limit_name.as_deref(), Some("codex_other"));
assert_eq!(
usage_limit
.rate_limits
.as_ref()
.and_then(|snapshot| snapshot.limit_name.as_deref()),
Some("codex_other")
);
}
#[test]
fn map_api_error_does_not_fallback_limit_name_to_limit_id() {
let mut headers = HeaderMap::new();
headers.insert(
ACTIVE_LIMIT_HEADER,
http::HeaderValue::from_static("codex_other"),
);
let body = serde_json::json!({
"error": {
"type": "usage_limit_reached",
"plan_type": "pro",
}
})
.to_string();
let err = map_api_error(ApiError::Transport(TransportError::Http {
status: http::StatusCode::TOO_MANY_REQUESTS,
url: Some("http://example.com/v1/responses".to_string()),
headers: Some(headers),
body: Some(body),
}));
let CodexErr::UsageLimitReached(usage_limit) = err else {
panic!("expected CodexErr::UsageLimitReached, got {err:?}");
};
assert_eq!(
usage_limit
.rate_limits
.as_ref()
.and_then(|snapshot| snapshot.limit_name.as_deref()),
None
);
}
}
+8
View File
@@ -1099,6 +1099,14 @@ impl Session {
});
}
maybe_push_unstable_features_warning(&config, &mut post_session_configured_events);
if config.approval_policy.value() == AskForApproval::OnFailure {
post_session_configured_events.push(Event {
id: "".to_owned(),
msg: EventMsg::Warning(WarningEvent {
message: "`on-failure` approval policy is deprecated and will be removed in a future release. Use `on-request` for interactive approvals or `never` for non-interactive runs.".to_string(),
}),
});
}
let auth = auth.as_ref();
let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from);
+14 -20
View File
@@ -411,18 +411,22 @@ pub struct UsageLimitReachedError {
pub(crate) resets_at: Option<DateTime<Utc>>,
pub(crate) rate_limits: Option<Box<RateLimitSnapshot>>,
pub(crate) promo_message: Option<String>,
pub(crate) limit_name: Option<String>,
}
impl std::fmt::Display for UsageLimitReachedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(limit_name) = self.limit_name.as_deref()
if let Some(limit_name) = self
.rate_limits
.as_ref()
.and_then(|snapshot| snapshot.limit_name.as_deref())
.map(str::trim)
.filter(|name| !name.is_empty())
&& !limit_name.eq_ignore_ascii_case("codex")
{
return write!(
f,
"You've hit your usage limit for {limit_name}.{}",
retry_suffix(self.resets_at.as_ref())
"You've hit your usage limit for {limit_name}. Switch to another model now,{}",
retry_suffix_after_or(self.resets_at.as_ref())
);
}
@@ -704,7 +708,6 @@ mod tests {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
assert_eq!(
err.to_string(),
@@ -822,7 +825,6 @@ mod tests {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
assert_eq!(
err.to_string(),
@@ -837,7 +839,6 @@ mod tests {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
assert_eq!(
err.to_string(),
@@ -852,7 +853,6 @@ mod tests {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
assert_eq!(
err.to_string(),
@@ -871,7 +871,6 @@ mod tests {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
let expected = format!(
"You've hit your usage limit. To get more access now, send a request to your admin or try again at {expected_time}."
@@ -887,7 +886,6 @@ mod tests {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
assert_eq!(
err.to_string(),
@@ -902,7 +900,6 @@ mod tests {
resets_at: None,
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
assert_eq!(
err.to_string(),
@@ -921,7 +918,6 @@ mod tests {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
let expected = format!(
"You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}."
@@ -939,15 +935,18 @@ mod tests {
let err = UsageLimitReachedError {
plan_type: Some(PlanType::Known(KnownPlan::Plus)),
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
rate_limits: Some(Box::new(RateLimitSnapshot {
limit_id: Some("codex_other".to_string()),
limit_name: Some("codex_other".to_string()),
..rate_limit_snapshot()
})),
promo_message: Some(
"Visit https://chatgpt.com/codex/settings/usage to purchase more credits"
.to_string(),
),
limit_name: Some("codex_other".to_string()),
};
let expected = format!(
"You've hit your usage limit for codex_other. Try again at {expected_time}."
"You've hit your usage limit for codex_other. Switch to another model now, or try again at {expected_time}."
);
assert_eq!(err.to_string(), expected);
});
@@ -964,7 +963,6 @@ mod tests {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
assert_eq!(err.to_string(), expected);
@@ -1074,7 +1072,6 @@ mod tests {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
let expected = format!(
"You've hit your usage limit. Upgrade to Pro (https://chatgpt.com/explore/pro), visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at {expected_time}."
@@ -1095,7 +1092,6 @@ mod tests {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
assert_eq!(err.to_string(), expected);
@@ -1113,7 +1109,6 @@ mod tests {
resets_at: Some(resets_at),
rate_limits: Some(Box::new(rate_limit_snapshot())),
promo_message: None,
limit_name: None,
};
let expected = format!("You've hit your usage limit. Try again at {expected_time}.");
assert_eq!(err.to_string(), expected);
@@ -1133,7 +1128,6 @@ mod tests {
promo_message: Some(
"To continue using Codex, start a free trial of <PLAN> today".to_string(),
),
limit_name: None,
};
let expected = format!(
"You've hit your usage limit. To continue using Codex, start a free trial of <PLAN> today, or try again at {expected_time}."
+1
View File
@@ -79,6 +79,7 @@ pub mod review_format;
pub mod review_prompts;
mod thread_manager;
pub mod web_search;
pub mod windows_sandbox_read_grants;
pub use codex_protocol::protocol::InitialHistory;
pub use thread_manager::NewThread;
pub use thread_manager::ThreadManager;
+32
View File
@@ -10,6 +10,7 @@ use codex_protocol::config_types::WindowsSandboxLevel;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
/// Kill switch for the elevated sandbox NUX on Windows.
///
@@ -200,6 +201,25 @@ pub fn run_legacy_setup_preflight(
)
}
#[cfg(target_os = "windows")]
pub fn run_setup_refresh_with_extra_read_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_setup_refresh_with_extra_read_roots(
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
extra_read_roots,
)
}
#[cfg(not(target_os = "windows"))]
pub fn run_legacy_setup_preflight(
_policy: &SandboxPolicy,
@@ -211,6 +231,18 @@ pub fn run_legacy_setup_preflight(
anyhow::bail!("legacy Windows sandbox setup is only supported on Windows")
}
#[cfg(not(target_os = "windows"))]
pub fn run_setup_refresh_with_extra_read_roots(
_policy: &SandboxPolicy,
_policy_cwd: &Path,
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
_extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
anyhow::bail!("Windows sandbox read-root refresh is only supported on Windows")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -0,0 +1,97 @@
use crate::protocol::SandboxPolicy;
use crate::windows_sandbox::run_setup_refresh_with_extra_read_roots;
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
pub fn grant_read_root_non_elevated(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
read_root: &Path,
) -> Result<PathBuf> {
if !read_root.is_absolute() {
anyhow::bail!("path must be absolute: {}", read_root.display());
}
if !read_root.exists() {
anyhow::bail!("path does not exist: {}", read_root.display());
}
if !read_root.is_dir() {
anyhow::bail!("path must be a directory: {}", read_root.display());
}
let canonical_root = dunce::canonicalize(read_root)?;
run_setup_refresh_with_extra_read_roots(
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
vec![canonical_root.clone()],
)?;
Ok(canonical_root)
}
#[cfg(test)]
mod tests {
use super::grant_read_root_non_elevated;
use crate::protocol::SandboxPolicy;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
fn policy() -> SandboxPolicy {
SandboxPolicy::new_workspace_write_policy()
}
#[test]
fn rejects_relative_path() {
let tmp = TempDir::new().expect("tempdir");
let err = grant_read_root_non_elevated(
&policy(),
tmp.path(),
tmp.path(),
&HashMap::new(),
tmp.path(),
Path::new("relative"),
)
.expect_err("relative path should fail");
assert!(err.to_string().contains("path must be absolute"));
}
#[test]
fn rejects_missing_path() {
let tmp = TempDir::new().expect("tempdir");
let missing = tmp.path().join("does-not-exist");
let err = grant_read_root_non_elevated(
&policy(),
tmp.path(),
tmp.path(),
&HashMap::new(),
tmp.path(),
missing.as_path(),
)
.expect_err("missing path should fail");
assert!(err.to_string().contains("path does not exist"));
}
#[test]
fn rejects_file_path() {
let tmp = TempDir::new().expect("tempdir");
let file_path = tmp.path().join("file.txt");
std::fs::write(&file_path, "hello").expect("write file");
let err = grant_read_root_non_elevated(
&policy(),
tmp.path(),
tmp.path(),
&HashMap::new(),
tmp.path(),
file_path.as_path(),
)
.expect_err("file path should fail");
assert!(err.to_string().contains("path must be a directory"));
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ cargo run -p codex-debug-client -- --thread-id thr_123
- `--codex-bin <path>`: path to the `codex` binary (default: `codex`).
- `-c, --config key=value`: pass through `--config` overrides to `codex`.
- `--thread-id <id>`: resume a thread instead of starting a new one.
- `--approval-policy <policy>`: `untrusted`, `on-failure`, `on-request`, `never`.
- `--approval-policy <policy>`: `untrusted`, `on-failure` (deprecated), `on-request`, `never`.
- `--auto-approve`: auto-approve command/file-change approvals (default: decline).
- `--final-only`: only show completed assistant messages and tool items.
- `--model <name>`: optional model override for thread start/resume.
+1 -1
View File
@@ -59,7 +59,7 @@ Request `newConversation` params (subset):
- `model`: string model id (e.g. "o3", "gpt-5.1", "gpt-5.1-codex")
- `profile`: optional named profile
- `cwd`: optional working directory
- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` | `never`
- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` (deprecated) | `never`
- `sandbox`: `read-only` | `workspace-write` | `external-sandbox` (honors `networkAccess` restricted/enabled) | `danger-full-access`
- `config`: map of additional config overrides
- `baseInstructions`: optional instruction override
+6 -4
View File
@@ -356,10 +356,12 @@ pub enum AskForApproval {
#[strum(serialize = "untrusted")]
UnlessTrusted,
/// *All* commands are auto‑approved, but they are expected to run inside a
/// sandbox where network access is disabled and writes are confined to a
/// specific set of paths. If the command fails, it will be escalated to
/// the user to approve execution without a sandbox.
/// DEPRECATED: *All* commands are auto‑approved, but they are expected to
/// run inside a sandbox where network access is disabled and writes are
/// confined to a specific set of paths. If the command fails, it will be
/// escalated to the user to approve execution without a sandbox.
/// Prefer `OnRequest` for interactive runs or `Never` for non-interactive
/// runs.
OnFailure,
/// The model decides when to ask the user for approval.
+57
View File
@@ -1826,6 +1826,63 @@ impl App {
let _ = preset;
}
}
AppEvent::BeginWindowsSandboxGrantReadRoot { path } => {
#[cfg(target_os = "windows")]
{
self.chat_widget
.add_to_history(history_cell::new_info_event(
format!("Granting sandbox read access to {path} ..."),
None,
));
let policy = self.config.sandbox_policy.get().clone();
let policy_cwd = self.config.cwd.clone();
let command_cwd = self.config.cwd.clone();
let env_map: std::collections::HashMap<String, String> =
std::env::vars().collect();
let codex_home = self.config.codex_home.clone();
let tx = self.app_event_tx.clone();
tokio::task::spawn_blocking(move || {
let requested_path = PathBuf::from(path);
let event = match codex_core::windows_sandbox_read_grants::grant_read_root_non_elevated(
&policy,
policy_cwd.as_path(),
command_cwd.as_path(),
&env_map,
codex_home.as_path(),
requested_path.as_path(),
) {
Ok(canonical_path) => AppEvent::WindowsSandboxGrantReadRootCompleted {
path: canonical_path,
error: None,
},
Err(err) => AppEvent::WindowsSandboxGrantReadRootCompleted {
path: requested_path,
error: Some(err.to_string()),
},
};
tx.send(event);
});
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
}
}
AppEvent::WindowsSandboxGrantReadRootCompleted { path, error } => match error {
Some(err) => {
self.chat_widget
.add_to_history(history_cell::new_error_event(format!("Error: {err}")));
}
None => {
self.chat_widget
.add_to_history(history_cell::new_info_event(
format!("Sandbox read access granted for {}", path.display()),
None,
));
}
},
AppEvent::EnableWindowsSandboxForAgentMode { preset, mode } => {
#[cfg(target_os = "windows")]
{
+13
View File
@@ -218,6 +218,19 @@ pub(crate) enum AppEvent {
preset: ApprovalPreset,
},
/// Begin a non-elevated grant of read access for an additional directory.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
BeginWindowsSandboxGrantReadRoot {
path: String,
},
/// Result of attempting to grant read access for an additional directory.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
WindowsSandboxGrantReadRootCompleted {
path: PathBuf,
error: Option<String>,
},
/// Enable the Windows sandbox feature and switch to Agent mode.
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
EnableWindowsSandboxForAgentMode {
+17
View File
@@ -3347,6 +3347,11 @@ impl ChatWidget {
// Not supported; on non-Windows this command should never be reachable.
};
}
SlashCommand::SandboxReadRoot => {
self.add_error_message(
"Usage: /sandbox-add-read-dir <absolute-directory-path>".to_string(),
);
}
SlashCommand::Experimental => {
self.open_experimental_popup();
}
@@ -3549,6 +3554,18 @@ impl ChatWidget {
});
self.bottom_pane.drain_pending_submission_state();
}
SlashCommand::SandboxReadRoot if !trimmed.is_empty() => {
let Some((prepared_args, _prepared_elements)) =
self.bottom_pane.prepare_inline_args_submission(false)
else {
return;
};
self.app_event_tx
.send(AppEvent::BeginWindowsSandboxGrantReadRoot {
path: prepared_args,
});
self.bottom_pane.drain_pending_submission_state();
}
_ => self.dispatch_command(cmd),
}
}
+12 -2
View File
@@ -17,6 +17,8 @@ pub enum SlashCommand {
Permissions,
#[strum(serialize = "setup-default-sandbox")]
ElevateSandbox,
#[strum(serialize = "sandbox-add-read-dir")]
SandboxReadRoot,
Experimental,
Skills,
Review,
@@ -84,7 +86,10 @@ impl SlashCommand {
SlashCommand::Agent => "switch the active agent thread",
SlashCommand::Approvals => "choose what Codex is allowed to do",
SlashCommand::Permissions => "choose what Codex is allowed to do",
SlashCommand::ElevateSandbox => "set up default agent sandbox",
SlashCommand::ElevateSandbox => "set up elevated agent sandbox",
SlashCommand::SandboxReadRoot => {
"let sandbox read a directory: /sandbox-add-read-dir <absolute_path>"
}
SlashCommand::Experimental => "toggle experimental features",
SlashCommand::Mcp => "list configured MCP tools",
SlashCommand::Apps => "manage apps",
@@ -104,7 +109,10 @@ impl SlashCommand {
pub fn supports_inline_args(self) -> bool {
matches!(
self,
SlashCommand::Review | SlashCommand::Rename | SlashCommand::Plan
SlashCommand::Review
| SlashCommand::Rename
| SlashCommand::Plan
| SlashCommand::SandboxReadRoot
)
}
@@ -122,6 +130,7 @@ impl SlashCommand {
| SlashCommand::Approvals
| SlashCommand::Permissions
| SlashCommand::ElevateSandbox
| SlashCommand::SandboxReadRoot
| SlashCommand::Experimental
| SlashCommand::Review
| SlashCommand::Plan
@@ -151,6 +160,7 @@ impl SlashCommand {
fn is_visible(self) -> bool {
match self {
SlashCommand::SandboxReadRoot => cfg!(target_os = "windows"),
SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions),
_ => true,
}
@@ -12,9 +12,10 @@ pub enum ApprovalModeCliArg {
/// is not in the "trusted" set.
Untrusted,
/// Run all commands without asking for user approval.
/// DEPRECATED: Run all commands without asking for user approval.
/// Only asks for approval if a command fails to execute, in which case it
/// will escalate to the user to ask for un-sandboxed execution.
/// Prefer `on-request` for interactive runs or `never` for non-interactive runs.
OnFailure,
/// The model decides when to ask the user for approval.
+2
View File
@@ -83,6 +83,8 @@ pub use setup::run_elevated_setup;
#[cfg(target_os = "windows")]
pub use setup::run_setup_refresh;
#[cfg(target_os = "windows")]
pub use setup::run_setup_refresh_with_extra_read_roots;
#[cfg(target_os = "windows")]
pub use setup::sandbox_dir;
#[cfg(target_os = "windows")]
pub use setup::sandbox_secrets_dir;
@@ -61,6 +61,47 @@ pub fn run_setup_refresh(
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Result<()> {
run_setup_refresh_inner(
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
None,
None,
)
}
pub fn run_setup_refresh_with_extra_read_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
extra_read_roots: Vec<PathBuf>,
) -> Result<()> {
let mut read_roots = gather_read_roots(command_cwd, policy);
read_roots.extend(extra_read_roots);
run_setup_refresh_inner(
policy,
policy_cwd,
command_cwd,
env_map,
codex_home,
Some(read_roots),
Some(Vec::new()),
)
}
fn run_setup_refresh_inner(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
read_roots_override: Option<Vec<PathBuf>>,
write_roots_override: Option<Vec<PathBuf>>,
) -> Result<()> {
// Skip in danger-full-access.
if matches!(
@@ -75,8 +116,8 @@ pub fn run_setup_refresh(
command_cwd,
env_map,
codex_home,
None,
None,
read_roots_override,
write_roots_override,
);
let payload = ElevationPayload {
version: SETUP_VERSION,