feat: add --add-dir flag for extra writable roots (#5335)
Add a `--add-dir` CLI flag so sessions can use extra writable roots in addition to the ones specified in the config file. These are ephemerally added during the session only. Fixes #3303 Fixes #2797
This commit is contained in:
committed by
GitHub
parent
2287d2afde
commit
4f46360aa4
@@ -61,6 +61,16 @@ In the transcript preview, the footer shows an `Esc edit prev` hint while editin
|
|||||||
|
|
||||||
Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session.
|
Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session.
|
||||||
|
|
||||||
|
### `--add-dir` flag
|
||||||
|
|
||||||
|
Need to work across multiple projects? Pass `--add-dir` one or more times to expose extra directories as writable roots for the current session while keeping the main working directory unchanged. For example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
codex --cd apps/frontend --add-dir ../backend --add-dir ../shared
|
||||||
|
```
|
||||||
|
|
||||||
|
Codex can now inspect and edit files in each listed directory without leaving the primary workspace.
|
||||||
|
|
||||||
### Shell completions
|
### Shell completions
|
||||||
|
|
||||||
Generate shell completion scripts via:
|
Generate shell completion scripts via:
|
||||||
|
|||||||
@@ -1353,6 +1353,7 @@ async fn derive_config_from_params(
|
|||||||
include_view_image_tool: None,
|
include_view_image_tool: None,
|
||||||
show_raw_agent_reasoning: None,
|
show_raw_agent_reasoning: None,
|
||||||
tools_web_search_request: None,
|
tools_web_search_request: None,
|
||||||
|
additional_writable_roots: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cli_overrides = cli_overrides
|
let cli_overrides = cli_overrides
|
||||||
|
|||||||
@@ -563,6 +563,9 @@ fn merge_resume_cli_flags(interactive: &mut TuiCli, resume_cli: TuiCli) {
|
|||||||
if !resume_cli.images.is_empty() {
|
if !resume_cli.images.is_empty() {
|
||||||
interactive.images = resume_cli.images;
|
interactive.images = resume_cli.images;
|
||||||
}
|
}
|
||||||
|
if !resume_cli.add_dir.is_empty() {
|
||||||
|
interactive.add_dir.extend(resume_cli.add_dir);
|
||||||
|
}
|
||||||
if let Some(prompt) = resume_cli.prompt {
|
if let Some(prompt) = resume_cli.prompt {
|
||||||
interactive.prompt = Some(prompt);
|
interactive.prompt = Some(prompt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ use codex_protocol::config_types::SandboxMode;
|
|||||||
use codex_protocol::config_types::Verbosity;
|
use codex_protocol::config_types::Verbosity;
|
||||||
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
|
use dunce::canonicalize;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use similar::DiffableStr;
|
use similar::DiffableStr;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -1094,6 +1095,8 @@ pub struct ConfigOverrides {
|
|||||||
pub include_view_image_tool: Option<bool>,
|
pub include_view_image_tool: Option<bool>,
|
||||||
pub show_raw_agent_reasoning: Option<bool>,
|
pub show_raw_agent_reasoning: Option<bool>,
|
||||||
pub tools_web_search_request: Option<bool>,
|
pub tools_web_search_request: Option<bool>,
|
||||||
|
/// Additional directories that should be treated as writable roots for this session.
|
||||||
|
pub additional_writable_roots: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -1122,6 +1125,7 @@ impl Config {
|
|||||||
include_view_image_tool: include_view_image_tool_override,
|
include_view_image_tool: include_view_image_tool_override,
|
||||||
show_raw_agent_reasoning,
|
show_raw_agent_reasoning,
|
||||||
tools_web_search_request: override_tools_web_search_request,
|
tools_web_search_request: override_tools_web_search_request,
|
||||||
|
additional_writable_roots,
|
||||||
} = overrides;
|
} = overrides;
|
||||||
|
|
||||||
let active_profile_name = config_profile_key
|
let active_profile_name = config_profile_key
|
||||||
@@ -1169,11 +1173,32 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let additional_writable_roots: Vec<PathBuf> = additional_writable_roots
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let absolute = if path.is_absolute() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
resolved_cwd.join(path)
|
||||||
|
};
|
||||||
|
match canonicalize(&absolute) {
|
||||||
|
Ok(canonical) => canonical,
|
||||||
|
Err(_) => absolute,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let active_project = cfg
|
let active_project = cfg
|
||||||
.get_active_project(&resolved_cwd)
|
.get_active_project(&resolved_cwd)
|
||||||
.unwrap_or(ProjectConfig { trust_level: None });
|
.unwrap_or(ProjectConfig { trust_level: None });
|
||||||
|
|
||||||
let sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode, &resolved_cwd);
|
let mut sandbox_policy = cfg.derive_sandbox_policy(sandbox_mode, &resolved_cwd);
|
||||||
|
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||||
|
for path in additional_writable_roots {
|
||||||
|
if !writable_roots.iter().any(|existing| existing == &path) {
|
||||||
|
writable_roots.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut approval_policy = approval_policy_override
|
let mut approval_policy = approval_policy_override
|
||||||
.or(config_profile.approval_policy)
|
.or(config_profile.approval_policy)
|
||||||
.or(cfg.approval_policy)
|
.or(cfg.approval_policy)
|
||||||
@@ -1613,6 +1638,46 @@ trust_level = "trusted"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<()> {
|
||||||
|
let temp_dir = TempDir::new()?;
|
||||||
|
let frontend = temp_dir.path().join("frontend");
|
||||||
|
let backend = temp_dir.path().join("backend");
|
||||||
|
std::fs::create_dir_all(&frontend)?;
|
||||||
|
std::fs::create_dir_all(&backend)?;
|
||||||
|
|
||||||
|
let overrides = ConfigOverrides {
|
||||||
|
cwd: Some(frontend),
|
||||||
|
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
|
||||||
|
additional_writable_roots: vec![PathBuf::from("../backend"), backend.clone()],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config::load_from_base_config_with_overrides(
|
||||||
|
ConfigToml::default(),
|
||||||
|
overrides,
|
||||||
|
temp_dir.path().to_path_buf(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let expected_backend = canonicalize(&backend).expect("canonicalize backend directory");
|
||||||
|
match config.sandbox_policy {
|
||||||
|
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||||
|
assert_eq!(
|
||||||
|
writable_roots
|
||||||
|
.iter()
|
||||||
|
.filter(|root| **root == expected_backend)
|
||||||
|
.count(),
|
||||||
|
1,
|
||||||
|
"expected single writable root entry for {}",
|
||||||
|
expected_backend.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected workspace-write policy, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn approve_all_feature_forces_on_request_policy() -> std::io::Result<()> {
|
fn approve_all_feature_forces_on_request_policy() -> std::io::Result<()> {
|
||||||
let cfg = r#"
|
let cfg = r#"
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
|||||||
include_view_image_tool: None,
|
include_view_image_tool: None,
|
||||||
show_raw_agent_reasoning: oss.then_some(true),
|
show_raw_agent_reasoning: oss.then_some(true),
|
||||||
tools_web_search_request: None,
|
tools_web_search_request: None,
|
||||||
|
additional_writable_roots: Vec::new(),
|
||||||
};
|
};
|
||||||
// Parse `-c` overrides.
|
// Parse `-c` overrides.
|
||||||
let cli_kv_overrides = match config_overrides.parse_overrides() {
|
let cli_kv_overrides = match config_overrides.parse_overrides() {
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ impl CodexToolCallParam {
|
|||||||
include_view_image_tool: None,
|
include_view_image_tool: None,
|
||||||
show_raw_agent_reasoning: None,
|
show_raw_agent_reasoning: None,
|
||||||
tools_web_search_request: None,
|
tools_web_search_request: None,
|
||||||
|
additional_writable_roots: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cli_overrides = cli_overrides
|
let cli_overrides = cli_overrides
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use clap::ValueHint;
|
||||||
use codex_common::ApprovalModeCliArg;
|
use codex_common::ApprovalModeCliArg;
|
||||||
use codex_common::CliConfigOverrides;
|
use codex_common::CliConfigOverrides;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -72,6 +73,10 @@ pub struct Cli {
|
|||||||
#[arg(long = "search", default_value_t = false)]
|
#[arg(long = "search", default_value_t = false)]
|
||||||
pub web_search: bool,
|
pub web_search: bool,
|
||||||
|
|
||||||
|
/// Additional directories that should be writable alongside the primary workspace.
|
||||||
|
#[arg(long = "add-dir", value_name = "DIR", value_hint = ValueHint::DirPath)]
|
||||||
|
pub add_dir: Vec<PathBuf>,
|
||||||
|
|
||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
pub config_overrides: CliConfigOverrides,
|
pub config_overrides: CliConfigOverrides,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ pub async fn run_main(
|
|||||||
|
|
||||||
// canonicalize the cwd
|
// canonicalize the cwd
|
||||||
let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p));
|
let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p));
|
||||||
|
let additional_dirs = cli.add_dir.clone();
|
||||||
|
|
||||||
let overrides = ConfigOverrides {
|
let overrides = ConfigOverrides {
|
||||||
model,
|
model,
|
||||||
@@ -177,6 +178,7 @@ pub async fn run_main(
|
|||||||
include_view_image_tool: None,
|
include_view_image_tool: None,
|
||||||
show_raw_agent_reasoning: cli.oss.then_some(true),
|
show_raw_agent_reasoning: cli.oss.then_some(true),
|
||||||
tools_web_search_request: cli.web_search.then_some(true),
|
tools_web_search_request: cli.web_search.then_some(true),
|
||||||
|
additional_writable_roots: additional_dirs,
|
||||||
};
|
};
|
||||||
let raw_overrides = cli.config_overrides.raw_overrides.clone();
|
let raw_overrides = cli.config_overrides.raw_overrides.clone();
|
||||||
let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };
|
let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };
|
||||||
|
|||||||
Reference in New Issue
Block a user