mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
merge local main into code-branch-20251006-105834 before merge
This commit is contained in:
+87
-23
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user