mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
feat: introduce ExternalSandbox policy (#8290)
## Description
Introduced `ExternalSandbox` policy to cover use case when sandbox
defined by outside environment, effectively it translates to
`SandboxMode#DangerFullAccess` for file system (since sandbox configured
on container level) and configurable `network_access` (either Restricted
or Enabled by outside environment).
as example you can configure `ExternalSandbox` policy as part of
`sendUserTurn` v1 app_server API:
```
{
"conversationId": <id>,
"cwd": <cwd>,
"approvalPolicy": "never",
"sandboxPolicy": {
"type": ""external-sandbox",
"network_access": "enabled"/"restricted"
},
"model": <model>,
"effort": <effort>,
....
}
```
This commit is contained in:
1 parent
3d4ced3ff5
commit
3429de21b3
26 files changed
+435
-39
No files matched your search
Generated
+3
-1
@@ -1244,6 +1244,8 @@ dependencies = [
|
|||||||
"codex-lmstudio",
|
"codex-lmstudio",
|
||||||
"codex-ollama",
|
"codex-ollama",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
|
"codex-utils-absolute-path",
|
||||||
|
"pretty_assertions",
|
||||||
"serde",
|
"serde",
|
||||||
"toml 0.9.5",
|
"toml 0.9.5",
|
||||||
]
|
]
|
||||||
@@ -1316,7 +1318,6 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"shlex",
|
"shlex",
|
||||||
"similar",
|
"similar",
|
||||||
"strum_macros 0.27.2",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"test-case",
|
"test-case",
|
||||||
"test-log",
|
"test-log",
|
||||||
@@ -1913,6 +1914,7 @@ dependencies = [
|
|||||||
"codex-utils-absolute-path",
|
"codex-utils-absolute-path",
|
||||||
"dirs-next",
|
"dirs-next",
|
||||||
"dunce",
|
"dunce",
|
||||||
|
"pretty_assertions",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus;
|
|||||||
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
||||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||||
|
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||||
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
|
||||||
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
|
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
|
||||||
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
||||||
@@ -470,6 +471,15 @@ pub enum ApprovalDecision {
|
|||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export_to = "v2/")]
|
||||||
|
pub enum NetworkAccess {
|
||||||
|
#[default]
|
||||||
|
Restricted,
|
||||||
|
Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
#[ts(tag = "type")]
|
#[ts(tag = "type")]
|
||||||
@@ -479,6 +489,12 @@ pub enum SandboxPolicy {
|
|||||||
ReadOnly,
|
ReadOnly,
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(rename_all = "camelCase")]
|
#[ts(rename_all = "camelCase")]
|
||||||
|
ExternalSandbox {
|
||||||
|
#[serde(default)]
|
||||||
|
network_access: NetworkAccess,
|
||||||
|
},
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(rename_all = "camelCase")]
|
||||||
WorkspaceWrite {
|
WorkspaceWrite {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
writable_roots: Vec<AbsolutePathBuf>,
|
writable_roots: Vec<AbsolutePathBuf>,
|
||||||
@@ -498,6 +514,14 @@ impl SandboxPolicy {
|
|||||||
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
|
||||||
}
|
}
|
||||||
SandboxPolicy::ReadOnly => codex_protocol::protocol::SandboxPolicy::ReadOnly,
|
SandboxPolicy::ReadOnly => codex_protocol::protocol::SandboxPolicy::ReadOnly,
|
||||||
|
SandboxPolicy::ExternalSandbox { network_access } => {
|
||||||
|
codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: match network_access {
|
||||||
|
NetworkAccess::Restricted => CoreNetworkAccess::Restricted,
|
||||||
|
NetworkAccess::Enabled => CoreNetworkAccess::Enabled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
SandboxPolicy::WorkspaceWrite {
|
SandboxPolicy::WorkspaceWrite {
|
||||||
writable_roots,
|
writable_roots,
|
||||||
network_access,
|
network_access,
|
||||||
@@ -520,6 +544,14 @@ impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
|
|||||||
SandboxPolicy::DangerFullAccess
|
SandboxPolicy::DangerFullAccess
|
||||||
}
|
}
|
||||||
codex_protocol::protocol::SandboxPolicy::ReadOnly => SandboxPolicy::ReadOnly,
|
codex_protocol::protocol::SandboxPolicy::ReadOnly => SandboxPolicy::ReadOnly,
|
||||||
|
codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => {
|
||||||
|
SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: match network_access {
|
||||||
|
CoreNetworkAccess::Restricted => NetworkAccess::Restricted,
|
||||||
|
CoreNetworkAccess::Enabled => NetworkAccess::Enabled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
|
||||||
writable_roots,
|
writable_roots,
|
||||||
network_access,
|
network_access,
|
||||||
@@ -1916,11 +1948,30 @@ mod tests {
|
|||||||
use codex_protocol::items::TurnItem;
|
use codex_protocol::items::TurnItem;
|
||||||
use codex_protocol::items::UserMessageItem;
|
use codex_protocol::items::UserMessageItem;
|
||||||
use codex_protocol::items::WebSearchItem;
|
use codex_protocol::items::WebSearchItem;
|
||||||
|
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
|
||||||
use codex_protocol::user_input::UserInput as CoreUserInput;
|
use codex_protocol::user_input::UserInput as CoreUserInput;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sandbox_policy_round_trips_external_sandbox_network_access() {
|
||||||
|
let v2_policy = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
let core_policy = v2_policy.to_core();
|
||||||
|
assert_eq!(
|
||||||
|
core_policy,
|
||||||
|
codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: CoreNetworkAccess::Enabled,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let back_to_v2 = SandboxPolicy::from(core_policy);
|
||||||
|
assert_eq!(back_to_v2, v2_policy);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn core_turn_item_into_thread_item_converts_supported_variants() {
|
fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||||
let user_item = TurnItem::UserMessage(UserMessageItem {
|
let user_item = TurnItem::UserMessage(UserMessageItem {
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ You can optionally specify config overrides on the new turn. If specified, these
|
|||||||
"cwd": "/Users/me/project",
|
"cwd": "/Users/me/project",
|
||||||
"approvalPolicy": "unlessTrusted",
|
"approvalPolicy": "unlessTrusted",
|
||||||
"sandboxPolicy": {
|
"sandboxPolicy": {
|
||||||
"mode": "workspaceWrite",
|
"type": "workspaceWrite",
|
||||||
"writableRoots": ["/Users/me/project"],
|
"writableRoots": ["/Users/me/project"],
|
||||||
"networkAccess": true
|
"networkAccess": true
|
||||||
},
|
},
|
||||||
@@ -285,10 +285,12 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin
|
|||||||
{ "id": 32, "result": { "exitCode": 0, "stdout": "...", "stderr": "" } }
|
{ "id": 32, "result": { "exitCode": 0, "stdout": "...", "stderr": "" } }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- For clients that are already sandboxed externally, set `sandboxPolicy` to `{"type":"externalSandbox","networkAccess":"enabled"}` (or omit `networkAccess` to keep it restricted). Codex will not enforce its own sandbox in this mode; it tells the model it has full file-system access and passes the `networkAccess` state through `environment_context`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Empty `command` arrays are rejected.
|
- Empty `command` arrays are rejected.
|
||||||
- `sandboxPolicy` accepts the same shape used by `turn/start` (e.g., `dangerFullAccess`, `readOnly`, `workspaceWrite` with flags).
|
- `sandboxPolicy` accepts the same shape used by `turn/start` (e.g., `dangerFullAccess`, `readOnly`, `workspaceWrite` with flags, `externalSandbox` with `networkAccess` `restricted|enabled`).
|
||||||
- When omitted, `timeoutMs` falls back to the server default.
|
- When omitted, `timeoutMs` falls back to the server default.
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|||||||
@@ -21,3 +21,10 @@ toml = { workspace = true, optional = true }
|
|||||||
cli = ["clap", "serde", "toml"]
|
cli = ["clap", "serde", "toml"]
|
||||||
elapsed = []
|
elapsed = []
|
||||||
sandbox_summary = []
|
sandbox_summary = []
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
clap = { workspace = true, features = ["derive", "wrap_help"] }
|
||||||
|
codex-utils-absolute-path = { workspace = true }
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
@@ -26,3 +26,22 @@ impl From<SandboxModeCliArg> for SandboxMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_cli_args_to_protocol_modes() {
|
||||||
|
assert_eq!(SandboxMode::ReadOnly, SandboxModeCliArg::ReadOnly.into());
|
||||||
|
assert_eq!(
|
||||||
|
SandboxMode::WorkspaceWrite,
|
||||||
|
SandboxModeCliArg::WorkspaceWrite.into()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
SandboxMode::DangerFullAccess,
|
||||||
|
SandboxModeCliArg::DangerFullAccess.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
|
use codex_core::protocol::NetworkAccess;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
|
|
||||||
pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
||||||
match sandbox_policy {
|
match sandbox_policy {
|
||||||
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
||||||
SandboxPolicy::ReadOnly => "read-only".to_string(),
|
SandboxPolicy::ReadOnly => "read-only".to_string(),
|
||||||
|
SandboxPolicy::ExternalSandbox { network_access } => {
|
||||||
|
let mut summary = "external-sandbox".to_string();
|
||||||
|
if matches!(network_access, NetworkAccess::Enabled) {
|
||||||
|
summary.push_str(" (network access enabled)");
|
||||||
|
}
|
||||||
|
summary
|
||||||
|
}
|
||||||
SandboxPolicy::WorkspaceWrite {
|
SandboxPolicy::WorkspaceWrite {
|
||||||
writable_roots,
|
writable_roots,
|
||||||
network_access,
|
network_access,
|
||||||
@@ -34,3 +42,45 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarizes_external_sandbox_without_network_access_suffix() {
|
||||||
|
let summary = summarize_sandbox_policy(&SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Restricted,
|
||||||
|
});
|
||||||
|
assert_eq!(summary, "external-sandbox");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarizes_external_sandbox_with_enabled_network() {
|
||||||
|
let summary = summarize_sandbox_policy(&SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Enabled,
|
||||||
|
});
|
||||||
|
assert_eq!(summary, "external-sandbox (network access enabled)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_write_summary_still_includes_network_access() {
|
||||||
|
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
|
||||||
|
let writable_root = AbsolutePathBuf::try_from(root).unwrap();
|
||||||
|
let summary = summarize_sandbox_policy(&SandboxPolicy::WorkspaceWrite {
|
||||||
|
writable_roots: vec![writable_root.clone()],
|
||||||
|
network_access: true,
|
||||||
|
exclude_tmpdir_env_var: true,
|
||||||
|
exclude_slash_tmp: true,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
summary,
|
||||||
|
format!(
|
||||||
|
"workspace-write [workdir, {}] (network access enabled)",
|
||||||
|
writable_root.to_string_lossy()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,6 @@ sha1 = { workspace = true }
|
|||||||
sha2 = { workspace = true }
|
sha2 = { workspace = true }
|
||||||
shlex = { workspace = true }
|
shlex = { workspace = true }
|
||||||
similar = { workspace = true }
|
similar = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
test-case = "3.3.1"
|
test-case = "3.3.1"
|
||||||
test-log = { workspace = true }
|
test-log = { workspace = true }
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ pub fn requires_initial_appoval(
|
|||||||
match policy {
|
match policy {
|
||||||
AskForApproval::Never | AskForApproval::OnFailure => false,
|
AskForApproval::Never | AskForApproval::OnFailure => false,
|
||||||
AskForApproval::OnRequest => {
|
AskForApproval::OnRequest => {
|
||||||
// In DangerFullAccess, only prompt if the command looks dangerous.
|
// In DangerFullAccess or ExternalSandbox, only prompt if the command looks dangerous.
|
||||||
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
|
if matches!(
|
||||||
|
sandbox_policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
) {
|
||||||
return command_might_be_dangerous(command);
|
return command_might_be_dangerous(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +86,7 @@ fn is_dangerous_to_call_with_exec(command: &[String]) -> bool {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use codex_protocol::protocol::NetworkAccess;
|
||||||
|
|
||||||
fn vec_str(items: &[&str]) -> Vec<String> {
|
fn vec_str(items: &[&str]) -> Vec<String> {
|
||||||
items.iter().map(std::string::ToString::to_string).collect()
|
items.iter().map(std::string::ToString::to_string).collect()
|
||||||
@@ -150,4 +154,23 @@ mod tests {
|
|||||||
fn rm_f_is_dangerous() {
|
fn rm_f_is_dangerous() {
|
||||||
assert!(command_might_be_dangerous(&vec_str(&["rm", "-f", "/"])));
|
assert!(command_might_be_dangerous(&vec_str(&["rm", "-f", "/"])));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_sandbox_only_prompts_for_dangerous_commands() {
|
||||||
|
let external_policy = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Restricted,
|
||||||
|
};
|
||||||
|
assert!(!requires_initial_appoval(
|
||||||
|
AskForApproval::OnRequest,
|
||||||
|
&external_policy,
|
||||||
|
&vec_str(&["ls"]),
|
||||||
|
SandboxPermissions::UseDefault,
|
||||||
|
));
|
||||||
|
assert!(requires_initial_appoval(
|
||||||
|
AskForApproval::OnRequest,
|
||||||
|
&external_policy,
|
||||||
|
&vec_str(&["rm", "-rf", "/"]),
|
||||||
|
SandboxPermissions::UseDefault,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use strum_macros::Display as DeriveDisplay;
|
|
||||||
|
|
||||||
use crate::codex::TurnContext;
|
use crate::codex::TurnContext;
|
||||||
use crate::protocol::AskForApproval;
|
use crate::protocol::AskForApproval;
|
||||||
|
use crate::protocol::NetworkAccess;
|
||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
use crate::shell::Shell;
|
use crate::shell::Shell;
|
||||||
use codex_protocol::config_types::SandboxMode;
|
use codex_protocol::config_types::SandboxMode;
|
||||||
@@ -12,15 +8,11 @@ use codex_protocol::models::ContentItem;
|
|||||||
use codex_protocol::models::ResponseItem;
|
use codex_protocol::models::ResponseItem;
|
||||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, DeriveDisplay)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
#[strum(serialize_all = "kebab-case")]
|
|
||||||
pub enum NetworkAccess {
|
|
||||||
Restricted,
|
|
||||||
Enabled,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename = "environment_context", rename_all = "snake_case")]
|
#[serde(rename = "environment_context", rename_all = "snake_case")]
|
||||||
pub(crate) struct EnvironmentContext {
|
pub(crate) struct EnvironmentContext {
|
||||||
@@ -45,12 +37,14 @@ impl EnvironmentContext {
|
|||||||
sandbox_mode: match sandbox_policy {
|
sandbox_mode: match sandbox_policy {
|
||||||
Some(SandboxPolicy::DangerFullAccess) => Some(SandboxMode::DangerFullAccess),
|
Some(SandboxPolicy::DangerFullAccess) => Some(SandboxMode::DangerFullAccess),
|
||||||
Some(SandboxPolicy::ReadOnly) => Some(SandboxMode::ReadOnly),
|
Some(SandboxPolicy::ReadOnly) => Some(SandboxMode::ReadOnly),
|
||||||
|
Some(SandboxPolicy::ExternalSandbox { .. }) => Some(SandboxMode::DangerFullAccess),
|
||||||
Some(SandboxPolicy::WorkspaceWrite { .. }) => Some(SandboxMode::WorkspaceWrite),
|
Some(SandboxPolicy::WorkspaceWrite { .. }) => Some(SandboxMode::WorkspaceWrite),
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
network_access: match sandbox_policy {
|
network_access: match sandbox_policy {
|
||||||
Some(SandboxPolicy::DangerFullAccess) => Some(NetworkAccess::Enabled),
|
Some(SandboxPolicy::DangerFullAccess) => Some(NetworkAccess::Enabled),
|
||||||
Some(SandboxPolicy::ReadOnly) => Some(NetworkAccess::Restricted),
|
Some(SandboxPolicy::ReadOnly) => Some(NetworkAccess::Restricted),
|
||||||
|
Some(SandboxPolicy::ExternalSandbox { network_access }) => Some(network_access),
|
||||||
Some(SandboxPolicy::WorkspaceWrite { network_access, .. }) => {
|
Some(SandboxPolicy::WorkspaceWrite { network_access, .. }) => {
|
||||||
if network_access {
|
if network_access {
|
||||||
Some(NetworkAccess::Enabled)
|
Some(NetworkAccess::Enabled)
|
||||||
@@ -272,6 +266,48 @@ mod tests {
|
|||||||
assert_eq!(context.serialize_to_xml(), expected);
|
assert_eq!(context.serialize_to_xml(), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_external_sandbox_environment_context() {
|
||||||
|
let context = EnvironmentContext::new(
|
||||||
|
None,
|
||||||
|
Some(AskForApproval::OnRequest),
|
||||||
|
Some(SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Enabled,
|
||||||
|
}),
|
||||||
|
fake_shell(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected = r#"<environment_context>
|
||||||
|
<approval_policy>on-request</approval_policy>
|
||||||
|
<sandbox_mode>danger-full-access</sandbox_mode>
|
||||||
|
<network_access>enabled</network_access>
|
||||||
|
<shell>bash</shell>
|
||||||
|
</environment_context>"#;
|
||||||
|
|
||||||
|
assert_eq!(context.serialize_to_xml(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialize_external_sandbox_with_restricted_network_environment_context() {
|
||||||
|
let context = EnvironmentContext::new(
|
||||||
|
None,
|
||||||
|
Some(AskForApproval::OnRequest),
|
||||||
|
Some(SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Restricted,
|
||||||
|
}),
|
||||||
|
fake_shell(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let expected = r#"<environment_context>
|
||||||
|
<approval_policy>on-request</approval_policy>
|
||||||
|
<sandbox_mode>danger-full-access</sandbox_mode>
|
||||||
|
<network_access>restricted</network_access>
|
||||||
|
<shell>bash</shell>
|
||||||
|
</environment_context>"#;
|
||||||
|
|
||||||
|
assert_eq!(context.serialize_to_xml(), expected);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serialize_full_access_environment_context() {
|
fn serialize_full_access_environment_context() {
|
||||||
let context = EnvironmentContext::new(
|
let context = EnvironmentContext::new(
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ pub async fn process_exec_tool_call(
|
|||||||
stdout_stream: Option<StdoutStream>,
|
stdout_stream: Option<StdoutStream>,
|
||||||
) -> Result<ExecToolCallOutput> {
|
) -> Result<ExecToolCallOutput> {
|
||||||
let sandbox_type = match &sandbox_policy {
|
let sandbox_type = match &sandbox_policy {
|
||||||
SandboxPolicy::DangerFullAccess => SandboxType::None,
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
|
SandboxType::None
|
||||||
|
}
|
||||||
_ => get_platform_sandbox().unwrap_or(SandboxType::None),
|
_ => get_platform_sandbox().unwrap_or(SandboxType::None),
|
||||||
};
|
};
|
||||||
tracing::debug!("Sandbox type: {sandbox_type:?}");
|
tracing::debug!("Sandbox type: {sandbox_type:?}");
|
||||||
@@ -523,7 +525,10 @@ async fn exec(
|
|||||||
) -> Result<RawExecToolCallOutput> {
|
) -> Result<RawExecToolCallOutput> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if sandbox == SandboxType::WindowsRestrictedToken
|
if sandbox == SandboxType::WindowsRestrictedToken
|
||||||
&& !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess)
|
&& !matches!(
|
||||||
|
sandbox_policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
)
|
||||||
{
|
{
|
||||||
return exec_windows_sandbox(params, sandbox_policy).await;
|
return exec_windows_sandbox(params, sandbox_policy).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ pub fn assess_patch_safety(
|
|||||||
if is_write_patch_constrained_to_writable_paths(action, sandbox_policy, cwd)
|
if is_write_patch_constrained_to_writable_paths(action, sandbox_policy, cwd)
|
||||||
|| policy == AskForApproval::OnFailure
|
|| policy == AskForApproval::OnFailure
|
||||||
{
|
{
|
||||||
if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) {
|
if matches!(
|
||||||
|
sandbox_policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
) {
|
||||||
// DangerFullAccess is intended to bypass sandboxing entirely.
|
// DangerFullAccess is intended to bypass sandboxing entirely.
|
||||||
SafetyCheck::AutoApprove {
|
SafetyCheck::AutoApprove {
|
||||||
sandbox_type: SandboxType::None,
|
sandbox_type: SandboxType::None,
|
||||||
@@ -147,7 +150,7 @@ fn is_write_patch_constrained_to_writable_paths(
|
|||||||
SandboxPolicy::ReadOnly => {
|
SandboxPolicy::ReadOnly => {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
SandboxPolicy::DangerFullAccess => {
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => sandbox_policy.get_writable_roots_with_cwd(cwd),
|
SandboxPolicy::WorkspaceWrite { .. } => sandbox_policy.get_writable_roots_with_cwd(cwd),
|
||||||
@@ -262,4 +265,23 @@ mod tests {
|
|||||||
&cwd,
|
&cwd,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_sandbox_auto_approves_in_on_request() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let cwd = tmp.path().to_path_buf();
|
||||||
|
let add_inside = ApplyPatchAction::new_add_for_test(&cwd.join("inner.txt"), "".to_string());
|
||||||
|
|
||||||
|
let policy = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: codex_protocol::protocol::NetworkAccess::Enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
assess_patch_safety(&add_inside, AskForApproval::OnRequest, &policy, &cwd,),
|
||||||
|
SafetyCheck::AutoApprove {
|
||||||
|
sandbox_type: SandboxType::None,
|
||||||
|
user_explicitly_approved: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,9 @@ impl SandboxManager {
|
|||||||
crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None)
|
crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None)
|
||||||
}
|
}
|
||||||
SandboxablePreference::Auto => match policy {
|
SandboxablePreference::Auto => match policy {
|
||||||
SandboxPolicy::DangerFullAccess => SandboxType::None,
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
|
SandboxType::None
|
||||||
|
}
|
||||||
_ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None),
|
_ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ pub(crate) fn default_exec_approval_requirement(
|
|||||||
) -> ExecApprovalRequirement {
|
) -> ExecApprovalRequirement {
|
||||||
let needs_approval = match policy {
|
let needs_approval = match policy {
|
||||||
AskForApproval::Never | AskForApproval::OnFailure => false,
|
AskForApproval::Never | AskForApproval::OnFailure => false,
|
||||||
AskForApproval::OnRequest => !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess),
|
AskForApproval::OnRequest => !matches!(
|
||||||
|
sandbox_policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
),
|
||||||
AskForApproval::UnlessTrusted => true,
|
AskForApproval::UnlessTrusted => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -253,3 +256,37 @@ impl<'a> SandboxAttempt<'a> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use codex_protocol::protocol::NetworkAccess;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_sandbox_skips_exec_approval_on_request() {
|
||||||
|
assert_eq!(
|
||||||
|
default_exec_approval_requirement(
|
||||||
|
AskForApproval::OnRequest,
|
||||||
|
&SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Restricted,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ExecApprovalRequirement::Skip {
|
||||||
|
bypass_sandbox: false,
|
||||||
|
proposed_execpolicy_amendment: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restricted_sandbox_requires_exec_approval_on_request() {
|
||||||
|
assert_eq!(
|
||||||
|
default_exec_approval_requirement(AskForApproval::OnRequest, &SandboxPolicy::ReadOnly),
|
||||||
|
ExecApprovalRequirement::NeedsApproval {
|
||||||
|
reason: None,
|
||||||
|
proposed_execpolicy_amendment: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ Request `newConversation` params (subset):
|
|||||||
- `profile`: optional named profile
|
- `profile`: optional named profile
|
||||||
- `cwd`: optional working directory
|
- `cwd`: optional working directory
|
||||||
- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` | `never`
|
- `approvalPolicy`: `untrusted` | `on-request` | `on-failure` | `never`
|
||||||
- `sandbox`: `read-only` | `workspace-write` | `danger-full-access`
|
- `sandbox`: `read-only` | `workspace-write` | `external-sandbox` (honors `networkAccess` restricted/enabled) | `danger-full-access`
|
||||||
- `config`: map of additional config overrides
|
- `config`: map of additional config overrides
|
||||||
- `baseInstructions`: optional instruction override
|
- `baseInstructions`: optional instruction override
|
||||||
- `compactPrompt`: optional replacement for the default compaction prompt
|
- `compactPrompt`: optional replacement for the default compaction prompt
|
||||||
|
|||||||
@@ -268,6 +268,24 @@ pub enum AskForApproval {
|
|||||||
Never,
|
Never,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents whether outbound network access is available to the agent.
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
pub enum NetworkAccess {
|
||||||
|
#[default]
|
||||||
|
Restricted,
|
||||||
|
Enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NetworkAccess {
|
||||||
|
pub fn is_enabled(self) -> bool {
|
||||||
|
matches!(self, NetworkAccess::Enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Determines execution restrictions for model shell commands.
|
/// Determines execution restrictions for model shell commands.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)]
|
||||||
#[strum(serialize_all = "kebab-case")]
|
#[strum(serialize_all = "kebab-case")]
|
||||||
@@ -281,6 +299,15 @@ pub enum SandboxPolicy {
|
|||||||
#[serde(rename = "read-only")]
|
#[serde(rename = "read-only")]
|
||||||
ReadOnly,
|
ReadOnly,
|
||||||
|
|
||||||
|
/// Indicates the process is already in an external sandbox. Allows full
|
||||||
|
/// disk access while honoring the provided network setting.
|
||||||
|
#[serde(rename = "external-sandbox")]
|
||||||
|
ExternalSandbox {
|
||||||
|
/// Whether the external sandbox permits outbound network traffic.
|
||||||
|
#[serde(default)]
|
||||||
|
network_access: NetworkAccess,
|
||||||
|
},
|
||||||
|
|
||||||
/// Same as `ReadOnly` but additionally grants write access to the current
|
/// Same as `ReadOnly` but additionally grants write access to the current
|
||||||
/// working directory ("workspace").
|
/// working directory ("workspace").
|
||||||
#[serde(rename = "workspace-write")]
|
#[serde(rename = "workspace-write")]
|
||||||
@@ -373,6 +400,7 @@ impl SandboxPolicy {
|
|||||||
pub fn has_full_disk_write_access(&self) -> bool {
|
pub fn has_full_disk_write_access(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
SandboxPolicy::DangerFullAccess => true,
|
SandboxPolicy::DangerFullAccess => true,
|
||||||
|
SandboxPolicy::ExternalSandbox { .. } => true,
|
||||||
SandboxPolicy::ReadOnly => false,
|
SandboxPolicy::ReadOnly => false,
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => false,
|
SandboxPolicy::WorkspaceWrite { .. } => false,
|
||||||
}
|
}
|
||||||
@@ -381,6 +409,7 @@ impl SandboxPolicy {
|
|||||||
pub fn has_full_network_access(&self) -> bool {
|
pub fn has_full_network_access(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
SandboxPolicy::DangerFullAccess => true,
|
SandboxPolicy::DangerFullAccess => true,
|
||||||
|
SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
|
||||||
SandboxPolicy::ReadOnly => false,
|
SandboxPolicy::ReadOnly => false,
|
||||||
SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
|
SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
|
||||||
}
|
}
|
||||||
@@ -392,6 +421,7 @@ impl SandboxPolicy {
|
|||||||
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
|
pub fn get_writable_roots_with_cwd(&self, cwd: &Path) -> Vec<WritableRoot> {
|
||||||
match self {
|
match self {
|
||||||
SandboxPolicy::DangerFullAccess => Vec::new(),
|
SandboxPolicy::DangerFullAccess => Vec::new(),
|
||||||
|
SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
|
||||||
SandboxPolicy::ReadOnly => Vec::new(),
|
SandboxPolicy::ReadOnly => Vec::new(),
|
||||||
SandboxPolicy::WorkspaceWrite {
|
SandboxPolicy::WorkspaceWrite {
|
||||||
writable_roots,
|
writable_roots,
|
||||||
@@ -1830,6 +1860,21 @@ mod tests {
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn external_sandbox_reports_full_access_flags() {
|
||||||
|
let restricted = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Restricted,
|
||||||
|
};
|
||||||
|
assert!(restricted.has_full_disk_write_access());
|
||||||
|
assert!(!restricted.has_full_network_access());
|
||||||
|
|
||||||
|
let enabled = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Enabled,
|
||||||
|
};
|
||||||
|
assert!(enabled.has_full_disk_write_access());
|
||||||
|
assert!(enabled.has_full_network_access());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn item_started_event_from_web_search_emits_begin_event() {
|
fn item_started_event_from_web_search_emits_begin_event() {
|
||||||
let event = ItemStartedEvent {
|
let event = ItemStartedEvent {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ pub fn add_dir_warning_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match sandbox_policy {
|
match sandbox_policy {
|
||||||
SandboxPolicy::WorkspaceWrite { .. } | SandboxPolicy::DangerFullAccess => None,
|
SandboxPolicy::WorkspaceWrite { .. }
|
||||||
|
| SandboxPolicy::DangerFullAccess
|
||||||
|
| SandboxPolicy::ExternalSandbox { .. } => None,
|
||||||
SandboxPolicy::ReadOnly => Some(format_warning(additional_dirs)),
|
SandboxPolicy::ReadOnly => Some(format_warning(additional_dirs)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +34,7 @@ fn format_warning(additional_dirs: &[PathBuf]) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::add_dir_warning_message;
|
use super::add_dir_warning_message;
|
||||||
|
use codex_core::protocol::NetworkAccess;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -50,6 +53,15 @@ mod tests {
|
|||||||
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
|
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_none_for_external_sandbox() {
|
||||||
|
let sandbox = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Enabled,
|
||||||
|
};
|
||||||
|
let dirs = vec![PathBuf::from("/tmp/example")];
|
||||||
|
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warns_for_read_only() {
|
fn warns_for_read_only() {
|
||||||
let sandbox = SandboxPolicy::ReadOnly;
|
let sandbox = SandboxPolicy::ReadOnly;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use chrono::Local;
|
|||||||
use codex_common::create_config_summary_entries;
|
use codex_common::create_config_summary_entries;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::openai_models::model_family::ModelFamily;
|
use codex_core::openai_models::model_family::ModelFamily;
|
||||||
|
use codex_core::protocol::NetworkAccess;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use codex_core::protocol::TokenUsage;
|
use codex_core::protocol::TokenUsage;
|
||||||
use codex_protocol::ConversationId;
|
use codex_protocol::ConversationId;
|
||||||
@@ -122,6 +123,13 @@ impl StatusHistoryCell {
|
|||||||
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
||||||
SandboxPolicy::ReadOnly => "read-only".to_string(),
|
SandboxPolicy::ReadOnly => "read-only".to_string(),
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
|
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
|
||||||
|
SandboxPolicy::ExternalSandbox { network_access } => {
|
||||||
|
if matches!(network_access, NetworkAccess::Enabled) {
|
||||||
|
"external-sandbox (network access enabled)".to_string()
|
||||||
|
} else {
|
||||||
|
"external-sandbox".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let agents_summary = compose_agents_summary(config);
|
let agents_summary = compose_agents_summary(config);
|
||||||
let account = compose_account_display(auth_manager, plan_type);
|
let account = compose_account_display(auth_manager, plan_type);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ pub fn add_dir_warning_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match sandbox_policy {
|
match sandbox_policy {
|
||||||
SandboxPolicy::WorkspaceWrite { .. } | SandboxPolicy::DangerFullAccess => None,
|
SandboxPolicy::WorkspaceWrite { .. }
|
||||||
|
| SandboxPolicy::DangerFullAccess
|
||||||
|
| SandboxPolicy::ExternalSandbox { .. } => None,
|
||||||
SandboxPolicy::ReadOnly => Some(format_warning(additional_dirs)),
|
SandboxPolicy::ReadOnly => Some(format_warning(additional_dirs)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +34,7 @@ fn format_warning(additional_dirs: &[PathBuf]) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::add_dir_warning_message;
|
use super::add_dir_warning_message;
|
||||||
|
use codex_core::protocol::NetworkAccess;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -50,6 +53,15 @@ mod tests {
|
|||||||
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
|
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn returns_none_for_external_sandbox() {
|
||||||
|
let sandbox = SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: NetworkAccess::Enabled,
|
||||||
|
};
|
||||||
|
let dirs = vec![PathBuf::from("/tmp/example")];
|
||||||
|
assert_eq!(add_dir_warning_message(&dirs, &sandbox), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warns_for_read_only() {
|
fn warns_for_read_only() {
|
||||||
let sandbox = SandboxPolicy::ReadOnly;
|
let sandbox = SandboxPolicy::ReadOnly;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use chrono::Local;
|
|||||||
use codex_common::create_config_summary_entries;
|
use codex_common::create_config_summary_entries;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::openai_models::model_family::ModelFamily;
|
use codex_core::openai_models::model_family::ModelFamily;
|
||||||
|
use codex_core::protocol::NetworkAccess;
|
||||||
use codex_core::protocol::SandboxPolicy;
|
use codex_core::protocol::SandboxPolicy;
|
||||||
use codex_core::protocol::TokenUsage;
|
use codex_core::protocol::TokenUsage;
|
||||||
use codex_protocol::ConversationId;
|
use codex_protocol::ConversationId;
|
||||||
@@ -122,6 +123,13 @@ impl StatusHistoryCell {
|
|||||||
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(),
|
||||||
SandboxPolicy::ReadOnly => "read-only".to_string(),
|
SandboxPolicy::ReadOnly => "read-only".to_string(),
|
||||||
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
|
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(),
|
||||||
|
SandboxPolicy::ExternalSandbox { network_access } => {
|
||||||
|
if matches!(network_access, NetworkAccess::Enabled) {
|
||||||
|
"external-sandbox (network access enabled)".to_string()
|
||||||
|
} else {
|
||||||
|
"external-sandbox".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let agents_summary = compose_agents_summary(config);
|
let agents_summary = compose_agents_summary(config);
|
||||||
let account = compose_account_display(auth_manager, plan_type);
|
let account = compose_account_display(auth_manager, plan_type);
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ features = [
|
|||||||
version = "0.52"
|
version = "0.52"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ pub fn apply_capability_denies_for_world_writable(
|
|||||||
})?,
|
})?,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
),
|
),
|
||||||
SandboxPolicy::DangerFullAccess => {
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ pub fn main() -> Result<()> {
|
|||||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||||
create_workspace_write_token_with_cap_from(base, psid_cap)
|
create_workspace_write_token_with_cap_from(base, psid_cap)
|
||||||
}
|
}
|
||||||
SandboxPolicy::DangerFullAccess => unreachable!(),
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (h_token, psid_to_use) = token_res?;
|
let (h_token, psid_to_use) = token_res?;
|
||||||
|
|||||||
@@ -239,8 +239,11 @@ mod windows_impl {
|
|||||||
require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?;
|
require_logon_sandbox_creds(&policy, sandbox_policy_cwd, cwd, &env_map, codex_home)?;
|
||||||
log_note("cli creds ready", logs_base_dir);
|
log_note("cli creds ready", logs_base_dir);
|
||||||
// Build capability SID for ACL grants.
|
// Build capability SID for ACL grants.
|
||||||
if matches!(&policy, SandboxPolicy::DangerFullAccess) {
|
if matches!(
|
||||||
anyhow::bail!("DangerFullAccess is not supported for sandboxing")
|
&policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
) {
|
||||||
|
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||||
}
|
}
|
||||||
let caps = load_or_create_cap_sids(codex_home)?;
|
let caps = load_or_create_cap_sids(codex_home)?;
|
||||||
let (psid_to_use, cap_sid_str) = match &policy {
|
let (psid_to_use, cap_sid_str) = match &policy {
|
||||||
@@ -252,7 +255,9 @@ mod windows_impl {
|
|||||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||||
caps.workspace.clone(),
|
caps.workspace.clone(),
|
||||||
),
|
),
|
||||||
SandboxPolicy::DangerFullAccess => unreachable!("DangerFullAccess handled above"),
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
|
unreachable!("DangerFullAccess handled above")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let AllowDenyPaths { allow: _, deny: _ } =
|
let AllowDenyPaths { allow: _, deny: _ } =
|
||||||
|
|||||||
@@ -194,8 +194,11 @@ mod windows_impl {
|
|||||||
log_start(&command, logs_base_dir);
|
log_start(&command, logs_base_dir);
|
||||||
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
|
||||||
|
|
||||||
if matches!(&policy, SandboxPolicy::DangerFullAccess) {
|
if matches!(
|
||||||
anyhow::bail!("DangerFullAccess is not supported for sandboxing")
|
&policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
) {
|
||||||
|
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||||
}
|
}
|
||||||
let caps = load_or_create_cap_sids(codex_home)?;
|
let caps = load_or_create_cap_sids(codex_home)?;
|
||||||
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
||||||
@@ -208,7 +211,9 @@ mod windows_impl {
|
|||||||
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||||
super::token::create_workspace_write_token_with_cap(psid)?
|
super::token::create_workspace_write_token_with_cap(psid)?
|
||||||
}
|
}
|
||||||
SandboxPolicy::DangerFullAccess => unreachable!("DangerFullAccess handled above"),
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||||
|
unreachable!("DangerFullAccess handled above")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,53 @@ pub fn parse_policy(value: &str) -> Result<SandboxPolicy> {
|
|||||||
match value {
|
match value {
|
||||||
"read-only" => Ok(SandboxPolicy::ReadOnly),
|
"read-only" => Ok(SandboxPolicy::ReadOnly),
|
||||||
"workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()),
|
"workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()),
|
||||||
"danger-full-access" => anyhow::bail!("DangerFullAccess is not supported for sandboxing"),
|
"danger-full-access" | "external-sandbox" => anyhow::bail!(
|
||||||
|
"DangerFullAccess and ExternalSandbox are not supported for sandboxing"
|
||||||
|
),
|
||||||
other => {
|
other => {
|
||||||
let parsed: SandboxPolicy = serde_json::from_str(other)?;
|
let parsed: SandboxPolicy = serde_json::from_str(other)?;
|
||||||
if matches!(parsed, SandboxPolicy::DangerFullAccess) {
|
if matches!(
|
||||||
anyhow::bail!("DangerFullAccess is not supported for sandboxing");
|
parsed,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
) {
|
||||||
|
anyhow::bail!(
|
||||||
|
"DangerFullAccess and ExternalSandbox are not supported for sandboxing"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Ok(parsed)
|
Ok(parsed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_external_sandbox_preset() {
|
||||||
|
let err = parse_policy("external-sandbox").unwrap_err();
|
||||||
|
assert!(err
|
||||||
|
.to_string()
|
||||||
|
.contains("DangerFullAccess and ExternalSandbox are not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_external_sandbox_json() {
|
||||||
|
let payload = serde_json::to_string(
|
||||||
|
&codex_protocol::protocol::SandboxPolicy::ExternalSandbox {
|
||||||
|
network_access: codex_protocol::protocol::NetworkAccess::Enabled,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let err = parse_policy(&payload).unwrap_err();
|
||||||
|
assert!(err
|
||||||
|
.to_string()
|
||||||
|
.contains("DangerFullAccess and ExternalSandbox are not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_read_only_policy() {
|
||||||
|
assert_eq!(parse_policy("read-only").unwrap(), SandboxPolicy::ReadOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,10 @@ pub fn run_setup_refresh(
|
|||||||
codex_home: &Path,
|
codex_home: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Skip in danger-full-access.
|
// Skip in danger-full-access.
|
||||||
if matches!(policy, SandboxPolicy::DangerFullAccess) {
|
if matches!(
|
||||||
|
policy,
|
||||||
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
||||||
|
) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let (read_roots, write_roots) = build_payload_roots(
|
let (read_roots, write_roots) = build_payload_roots(
|
||||||
|
|||||||
Reference in new issue
Block a user