mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge 5032154ecf into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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}."
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")]
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user