merge local main into code-branch-20251006-105834 before merge

This commit is contained in:
James Peter
2025-10-07 07:46:37 +10:00
6 changed files with 406 additions and 34 deletions
+87 -23
View File
@@ -2287,13 +2287,10 @@ impl Config {
}
fn load_instructions(code_dir: Option<&Path>) -> Option<String> {
let mut p = match code_dir {
Some(p) => p.to_path_buf(),
None => return None,
};
let code_home = code_dir?;
let read_path = resolve_code_path_for_read(code_home, Path::new("AGENTS.md"));
p.push("AGENTS.md");
std::fs::read_to_string(&p).ok().and_then(|s| {
std::fs::read_to_string(&read_path).ok().and_then(|s| {
let s = s.trim();
if s.is_empty() {
None
@@ -2365,24 +2362,34 @@ fn env_overrides_present() -> bool {
|| matches!(std::env::var("CODEX_HOME"), Ok(ref v) if !v.trim().is_empty())
}
fn compute_legacy_code_home_dir() -> Option<PathBuf> {
if env_overrides_present() {
return None;
}
let Some(home) = home_dir() else {
return None;
};
let candidate = home.join(".codex");
if path_exists(&candidate) {
Some(candidate)
} else {
None
}
}
fn legacy_code_home_dir() -> Option<PathBuf> {
static LEGACY: OnceLock<Option<PathBuf>> = OnceLock::new();
LEGACY
.get_or_init(|| {
if env_overrides_present() {
return None;
}
let Some(home) = home_dir() else {
return None;
};
let candidate = home.join(".codex");
if path_exists(&candidate) {
Some(candidate)
} else {
None
}
})
.clone()
#[cfg(test)]
{
return compute_legacy_code_home_dir();
}
#[cfg(not(test))]
{
static LEGACY: OnceLock<Option<PathBuf>> = OnceLock::new();
LEGACY
.get_or_init(compute_legacy_code_home_dir)
.clone()
}
}
fn path_exists(path: &Path) -> bool {
@@ -2456,8 +2463,32 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::ffi::OsString;
use tempfile::TempDir;
struct EnvVarGuard {
key: &'static str,
original: Option<OsString>,
}
impl EnvVarGuard {
fn new(key: &'static str) -> Self {
Self {
key,
original: std::env::var_os(key),
}
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.original {
Some(val) => std::env::set_var(self.key, val),
None => std::env::remove_var(self.key),
}
}
}
#[test]
fn test_toml_parsing() {
let history_with_persistence = r#"
@@ -2508,6 +2539,39 @@ persistence = "none"
assert_eq!(parsed_bool.auto_upgrade_enabled, Some(true));
}
#[test]
fn load_instructions_reads_from_code_home() -> anyhow::Result<()> {
let code_home = TempDir::new()?;
std::fs::write(code_home.path().join("AGENTS.md"), " keep me \n")?;
let loaded = Config::load_instructions(Some(code_home.path()));
assert_eq!(loaded.as_deref(), Some("keep me"));
Ok(())
}
#[test]
fn load_instructions_falls_back_to_legacy_codex_home() -> anyhow::Result<()> {
let code_home = TempDir::new()?;
let legacy_home = TempDir::new()?;
let legacy_codex = legacy_home.path().join(".codex");
std::fs::create_dir_all(&legacy_codex)?;
std::fs::write(legacy_codex.join("AGENTS.md"), " legacy guidance \n")?;
let _home_guard = EnvVarGuard::new("HOME");
let _code_home_guard = EnvVarGuard::new("CODE_HOME");
let _codex_home_guard = EnvVarGuard::new("CODEX_HOME");
std::env::set_var("HOME", legacy_home.path());
std::env::remove_var("CODE_HOME");
std::env::remove_var("CODEX_HOME");
let loaded = Config::load_instructions(Some(code_home.path()));
assert_eq!(loaded.as_deref(), Some("legacy guidance"));
Ok(())
}
#[test]
fn tui_config_missing_notifications_field_defaults_to_disabled() {
let cfg = r#"
@@ -1505,6 +1505,9 @@ impl ChatComposer {
modifiers: KeyModifiers::NONE,
..
} => {
if self.handle_backslash_continuation() {
return (InputResult::None, true);
}
let command_text = self.textarea.text().to_string();
let first_line = command_text.lines().next().unwrap_or("");
if let Some((name, rest)) = parse_slash_name(first_line)
@@ -1558,6 +1561,55 @@ impl ChatComposer {
}
}
fn handle_backslash_continuation(&mut self) -> bool {
let text = self.textarea.text();
if text.is_empty() {
return false;
}
let mut iter = text.char_indices().rev();
let Some((last_idx, last_char)) = iter.next() else {
return false;
};
if matches!(last_char, ' ' | '\t') {
return false;
}
if last_char != '\\' {
return false;
}
let trailing_backslashes = text[..last_idx]
.chars()
.rev()
.take_while(|c| *c == '\\')
.count()
+ 1;
if trailing_backslashes % 2 == 0 {
return false;
}
let line_start = text[..last_idx].rfind('\n').map(|idx| idx + 1).unwrap_or(0);
let line_before = &text[line_start..last_idx];
let indentation: String = line_before
.chars()
.take_while(|c| matches!(*c, ' ' | '\t'))
.collect();
let replacement = if indentation.is_empty() {
String::from("\n")
} else {
format!("\n{indentation}")
};
let backslash_end = last_idx + '\\'.len_utf8();
self.textarea.replace_range(last_idx..backslash_end, &replacement);
self.history.reset_navigation();
self.post_paste_space_guard = None;
self.pending_pastes
.retain(|(placeholder, _)| self.textarea.text().contains(placeholder));
self.typed_anything = true;
true
}
/// Handle generic Input events that modify the textarea content.
fn handle_input_basic(&mut self, input: KeyEvent) -> (InputResult, bool) {
let text_before = self.textarea.text().to_string();
+190 -10
View File
@@ -10,6 +10,7 @@ use code_core::config::Config;
use code_core::config::ConfigOverrides;
use code_core::config::ConfigToml;
use code_core::config::find_code_home;
use code_core::config::load_config_as_toml;
use code_core::config::load_config_as_toml_with_cli_overrides;
use code_core::protocol::AskForApproval;
use code_core::protocol::SandboxPolicy;
@@ -290,6 +291,33 @@ pub async fn run_main(
.iter()
.any(|(path, _)| path.starts_with("tui.theme"));
let code_home = match find_code_home() {
Ok(code_home) => code_home,
#[allow(clippy::print_stderr)]
Err(err) => {
eprintln!("Error finding codex home: {err}");
std::process::exit(1);
}
};
let workspace_write_network_access_explicit = {
let cli_override = cli_kv_overrides
.iter()
.any(|(path, _)| path == "sandbox_workspace_write.network_access");
if cli_override {
true
} else {
match load_config_as_toml(&code_home) {
Ok(raw) => raw
.get("sandbox_workspace_write")
.and_then(|value| value.as_table())
.map_or(false, |table| table.contains_key("network_access")),
Err(_) => false,
}
}
};
let mut config = {
// Load configuration and support CLI overrides.
@@ -308,17 +336,9 @@ pub async fn run_main(
// we load config.toml here to determine project state.
#[allow(clippy::print_stderr)]
let (config_toml, theme_set_in_config_file) = {
let code_home = match find_code_home() {
Ok(code_home) => code_home,
Err(err) => {
eprintln!("Error finding codex home: {err}");
std::process::exit(1);
}
};
let theme_set_in_config_file = theme_configured_in_config_file(&code_home);
match load_config_as_toml_with_cli_overrides(&code_home, cli_kv_overrides) {
match load_config_as_toml_with_cli_overrides(&code_home, cli_kv_overrides.clone()) {
Ok(config_toml) => (config_toml, theme_set_in_config_file),
Err(err) => {
eprintln!("Error loading config.toml: {err}");
@@ -335,6 +355,7 @@ pub async fn run_main(
approval_policy,
sandbox_mode,
cli.config_profile.clone(),
workspace_write_network_access_explicit,
)?;
let log_dir = code_core::config::log_dir(&config)?;
@@ -697,6 +718,7 @@ fn determine_repo_trust_state(
approval_policy_overide: Option<AskForApproval>,
sandbox_mode_override: Option<SandboxMode>,
config_profile_override: Option<String>,
workspace_write_network_access_explicit: bool,
) -> std::io::Result<bool> {
let config_profile = config_toml.get_config_profile(config_profile_override)?;
@@ -729,6 +751,31 @@ fn determine_repo_trust_state(
// If the current cwd project is trusted and no explicit config has been set,
// default to fully trusted, non‑interactive execution to match expected behavior.
// This restores the previous semantics before the recent trust‑flow refactor.
if let Some(workspace_write) = config_toml.sandbox_workspace_write.as_ref() {
// Honor explicit sandbox WorkspaceWrite protections (like allow_git_writes = false)
// even when the project is marked as trusted.
if !workspace_write.allow_git_writes {
// Maintain the historical networking behaviour from DangerFullAccess: allow outbound
// network even when we pivot into WorkspaceWrite solely to protect `.git`.
let network_access = if workspace_write.network_access {
true
} else if workspace_write_network_access_explicit {
false
} else {
true
};
config.approval_policy = AskForApproval::Never;
config.sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: workspace_write.writable_roots.clone(),
network_access,
exclude_tmpdir_env_var: workspace_write.exclude_tmpdir_env_var,
exclude_slash_tmp: workspace_write.exclude_slash_tmp,
allow_git_writes: workspace_write.allow_git_writes,
};
return Ok(false);
}
}
config.approval_policy = AskForApproval::Never;
config.sandbox_policy = SandboxPolicy::DangerFullAccess;
Ok(false)
@@ -737,4 +784,137 @@ fn determine_repo_trust_state(
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
use code_core::config::ProjectConfig;
use code_core::config_types::SandboxWorkspaceWrite;
use code_core::protocol::AskForApproval;
use std::collections::HashMap;
use tempfile::TempDir;
fn make_trusted_config(
sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
) -> std::io::Result<(Config, ConfigToml)> {
let code_home = TempDir::new()?;
let workspace = TempDir::new()?;
let mut config_toml = ConfigToml::default();
config_toml.sandbox_workspace_write = sandbox_workspace_write;
let mut projects = HashMap::new();
projects.insert(
workspace.path().to_string_lossy().to_string(),
ProjectConfig {
trust_level: Some("trusted".to_string()),
approval_policy: None,
sandbox_mode: None,
always_allow_commands: None,
hooks: vec![],
commands: vec![],
},
);
config_toml.projects = Some(projects);
let overrides = ConfigOverrides {
cwd: Some(workspace.path().to_path_buf()),
..Default::default()
};
let config = Config::load_from_base_config_with_overrides(
config_toml.clone(),
overrides,
code_home.path().to_path_buf(),
)?;
Ok((config, config_toml))
}
#[test]
fn trusted_workspace_honors_allow_git_writes_override() -> std::io::Result<()> {
let (mut config, config_toml) = make_trusted_config(Some(SandboxWorkspaceWrite {
allow_git_writes: false,
..Default::default()
}))?;
let show_trust = determine_repo_trust_state(
&mut config,
&config_toml,
None,
None,
None,
false,
)?;
assert!(!show_trust);
match &config.sandbox_policy {
SandboxPolicy::WorkspaceWrite {
allow_git_writes,
network_access,
..
} => {
assert!(!allow_git_writes);
assert!(*network_access, "trusted WorkspaceWrite should retain network access");
}
other => panic!("expected workspace-write sandbox, got {other:?}"),
}
assert!(matches!(config.approval_policy, AskForApproval::Never));
Ok(())
}
#[test]
fn trusted_workspace_default_stays_danger_full_access() -> std::io::Result<()> {
let (mut config, config_toml) = make_trusted_config(None)?;
let show_trust = determine_repo_trust_state(
&mut config,
&config_toml,
None,
None,
None,
false,
)?;
assert!(!show_trust);
assert!(matches!(config.sandbox_policy, SandboxPolicy::DangerFullAccess));
assert!(matches!(config.approval_policy, AskForApproval::Never));
Ok(())
}
#[test]
fn trusted_workspace_respects_explicit_network_disable() -> std::io::Result<()> {
let (mut config, config_toml) = make_trusted_config(Some(SandboxWorkspaceWrite {
allow_git_writes: false,
network_access: false,
..Default::default()
}))?;
let show_trust = determine_repo_trust_state(
&mut config,
&config_toml,
None,
None,
None,
true,
)?;
assert!(!show_trust);
match &config.sandbox_policy {
SandboxPolicy::WorkspaceWrite {
allow_git_writes,
network_access,
..
} => {
assert!(!allow_git_writes);
assert!(!network_access, "explicit opt-out should disable network access");
}
other => panic!("expected workspace-write sandbox, got {other:?}"),
}
Ok(())
}
}
@@ -103,6 +103,11 @@ impl ComposerInput {
WidgetRef::render_ref(&self.inner, area, buf);
}
/// Return the current text contents of the composer.
pub fn text(&self) -> &str {
self.inner.text()
}
/// Return true if a paste-burst detection is currently active.
pub fn is_in_paste_burst(&self) -> bool {
self.inner.is_in_paste_burst()
+71
View File
@@ -208,6 +208,77 @@ fn composer_input_shift_enter_no_submit() {
}
}
#[test]
fn composer_input_backslash_continuation_inserts_newline() {
let mut composer = ComposerInput::new();
composer.handle_paste("Hello\\".to_string());
let enter = make_key(KeyCode::Enter, KeyModifiers::NONE);
match composer.input(enter) {
ComposerAction::None => {
assert_eq!(composer.text(), "Hello\n");
assert!(!composer.is_empty(), "composer should retain text after continuation");
}
ComposerAction::Submitted(_) => panic!("continuation should not submit"),
}
}
#[test]
fn composer_input_backslash_continuation_trailing_space_submits() {
let mut composer = ComposerInput::new();
composer.handle_paste("Hello\\ ".to_string());
match composer.input(make_key(KeyCode::Enter, KeyModifiers::NONE)) {
ComposerAction::Submitted(text) => {
assert_eq!(text, "Hello\\ ");
assert!(composer.is_empty(), "composer should clear after submission");
}
ComposerAction::None => panic!("trailing space should cancel continuation"),
}
}
#[test]
fn composer_input_backslash_even_count_submits() {
let mut composer = ComposerInput::new();
composer.handle_paste(String::from(r"Path: C\\"));
match composer.input(make_key(KeyCode::Enter, KeyModifiers::NONE)) {
ComposerAction::Submitted(text) => assert_eq!(text, String::from(r"Path: C\\")),
ComposerAction::None => panic!("even number of backslashes should submit"),
}
}
#[test]
fn composer_input_backslash_multiple_continuations() {
let mut composer = ComposerInput::new();
composer.handle_paste("line1\\".to_string());
assert!(matches!(
composer.input(make_key(KeyCode::Enter, KeyModifiers::NONE)),
ComposerAction::None
));
assert_eq!(composer.text(), "line1\n");
composer.handle_paste("line2\\".to_string());
assert!(matches!(
composer.input(make_key(KeyCode::Enter, KeyModifiers::NONE)),
ComposerAction::None
));
assert_eq!(composer.text(), "line1\nline2\n");
}
#[test]
fn composer_input_backslash_continuation_preserves_indent() {
let mut composer = ComposerInput::new();
composer.handle_paste(" indented\\".to_string());
assert!(matches!(
composer.input(make_key(KeyCode::Enter, KeyModifiers::NONE)),
ComposerAction::None
));
assert_eq!(composer.text(), " indented\n ");
}
#[test]
fn composer_input_ctrl_c_aborts_without_submit() {
let mut composer = ComposerInput::new();
+1 -1
View File
@@ -7,5 +7,5 @@ Please use gh to review this issue in https://github.com/just-every/code/issues/
- We may have already fixed the issue
- Any PRs created were performed by an automated build. They may resolve the issue, but don't assume that just because they exist the issue is resolved
- If we haven't fixed the issue and there's no automated PR which fixes it, then please write a fix in this branch
- Once complete, validate with build-fast.hs and commit the change (with commit message fixing issue), but don't push
- Once complete, validate with build-fast.hs and commit the changes with commit message closing the issue, but don't push
3. Draft a response to the user based on findings above - if fixed, mention it will be out in next release. Don't suggest they build themselves.