Compare commits

...

4 Commits

Author SHA1 Message Date
Owen Lin
8e9fa727bf update 2026-05-06 17:30:02 -07:00
Owen Lin
9cd26d6fa1 add remote-control command 2026-05-06 15:21:05 -07:00
Brian Henzelmann
8f5d68f9d2 Document Codex git commit attribution config (#21379)
## Summary
- document that commit attribution for generated git commit messages is
gated by the `codex_git_commit` feature flag
- add an example `config.toml` snippet showing `commit_attribution` with
`[features].codex_git_commit = true`
- update the config schema description so the reference docs explain
that `commit_attribution` only takes effect when the feature is enabled

Fixes #19799.

## Validation
- `cargo run -p codex-core --bin codex-write-config-schema`
- `cargo test -p codex-config`
- `cargo test -p codex-features`
- `cargo fmt --check`
- `git diff --check`

## Notes
- `cargo test -p codex-core config_schema_matches_fixture` currently
fails before reaching the schema test because `core_test_support`
imports `similar` without a linked crate in this checkout. The narrower
package checks above avoid that unrelated test-support build failure.
2026-05-06 16:14:50 -05:00
iceweasel-oai
123e78b97b [codex] Fix Windows sandbox git safe.directory for worktrees (#21409)
## Why

Windows sandboxed commands run as a sandbox user, while workspace
repositories are usually owned by the real user. The sandbox compensates
by injecting a temporary Git `safe.directory` entry into the child
environment.

That injection was still broken for linked worktrees because the helper
followed the `.git` file's `gitdir:` pointer and injected the internal
`.git/worktrees/...` location. Git's dubious-ownership check expects the
worktree root instead, so sandboxed Git commands still failed in
worktree-based Codex checkouts.

## What changed

- Treat any `.git` marker, directory or file, as the worktree root for
`safe.directory` injection.
- Keep the safe-directory logic in
`windows-sandbox-rs/src/sandbox_utils.rs` and have the one-shot elevated
path reuse it.
- Add regression coverage for both normal `.git` directories and
gitfile-based worktrees.

## Validation

- `cargo test -p codex-windows-sandbox sandbox_utils::tests`
- `cargo test -p codex-windows-sandbox` built and ran; the new
`sandbox_utils` tests passed, while two pre-existing legacy sandbox
tests failed locally with `Access is denied`:
`session::tests::legacy_non_tty_cmd_emits_output` and
`spawn_prep::tests::legacy_spawn_env_applies_offline_network_rewrite`.
2026-05-06 14:08:45 -07:00
7 changed files with 167 additions and 85 deletions

View File

@@ -130,6 +130,9 @@ enum Subcommand {
/// [experimental] Run the app server or related tooling.
AppServer(AppServerCommand),
/// [experimental] Start a headless app-server with remote control enabled.
RemoteControl,
/// Launch the Codex desktop app (opens the app installer if missing).
#[cfg(any(target_os = "macos", target_os = "windows"))]
App(app_cmd::AppCommand),
@@ -736,6 +739,14 @@ struct FeatureSetArgs {
feature: String,
}
const REMOTE_CONTROL_FEATURE_OVERRIDE: &str = "features.remote_control=true";
fn enable_remote_control_for_invocation(config_overrides: &mut CliConfigOverrides) {
config_overrides
.raw_overrides
.push(REMOTE_CONTROL_FEATURE_OVERRIDE.to_string());
}
fn stage_str(stage: Stage) -> &'static str {
match stage {
Stage::UnderDevelopment => "under development",
@@ -916,6 +927,24 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
}
}
}
Some(Subcommand::RemoteControl) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
root_remote_auth_token_env.as_deref(),
"remote-control",
)?;
enable_remote_control_for_invocation(&mut root_config_overrides);
codex_app_server::run_main_with_transport(
arg0_paths.clone(),
root_config_overrides,
codex_config::LoaderOverrides::default(),
/*default_analytics_enabled*/ false,
codex_app_server::AppServerTransport::Off,
codex_protocol::protocol::SessionSource::Cli,
codex_app_server::AppServerWebsocketAuthSettings::default(),
)
.await?;
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
Some(Subcommand::App(app_cli)) => {
reject_remote_mode_for_subcommand(
@@ -2296,6 +2325,45 @@ mod tests {
assert!(app_server.analytics_default_enabled);
}
#[test]
fn remote_control_override_is_appended_after_root_toggles() {
let mut config_overrides = CliConfigOverrides::default();
config_overrides
.raw_overrides
.push("features.remote_control=false".to_string());
enable_remote_control_for_invocation(&mut config_overrides);
assert_eq!(
config_overrides.raw_overrides,
vec![
"features.remote_control=false".to_string(),
REMOTE_CONTROL_FEATURE_OVERRIDE.to_string(),
]
);
}
#[test]
fn reject_remote_flag_for_remote_control() {
let cli = MultitoolCli::try_parse_from([
"codex",
"--remote",
"ws://127.0.0.1:1234",
"remote-control",
])
.expect("parse");
assert_matches!(cli.subcommand, Some(Subcommand::RemoteControl));
let err = reject_remote_mode_for_subcommand(
cli.remote.remote.as_deref(),
cli.remote.remote_auth_token_env.as_deref(),
"remote-control",
)
.expect_err("remote-control should reject root --remote");
assert!(err.to_string().contains("remote-control"));
}
#[test]
fn remote_flag_parses_for_interactive_root() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "ws://127.0.0.1:4500"])

View File

@@ -176,7 +176,10 @@ pub struct ConfigToml {
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
/// This top-level setting only takes effect when `[features].codex_git_commit`
/// is enabled.
///
/// When enabled and unset, Codex uses `Codex <noreply@openai.com>`.
/// Set to an empty string to disable automatic commit attribution.
pub commit_attribution: Option<String>,

View File

@@ -3838,7 +3838,7 @@
"description": "Preferred backend for storing CLI auth credentials. file (default): Use a file in the Codex home directory. keyring: Use an OS-specific keyring service. auto: Use the keyring if available, otherwise use a file."
},
"commit_attribution": {
"description": "Optional commit attribution text for commit message co-author trailers.\n\nSet to an empty string to disable automatic commit attribution.",
"description": "Optional commit attribution text for commit message co-author trailers. This top-level setting only takes effect when `[features].codex_git_commit` is enabled.\n\nWhen enabled and unset, Codex uses `Codex <noreply@openai.com>`. Set to an empty string to disable automatic commit attribution.",
"type": "string"
},
"compact_prompt": {

View File

@@ -476,6 +476,8 @@ pub struct Config {
pub compact_prompt: Option<String>,
/// Optional commit attribution text for commit message co-author trailers.
/// This top-level setting only takes effect when `[features].codex_git_commit`
/// is enabled.
///
/// - `None`: use default attribution (`Codex <noreply@openai.com>`)
/// - `Some("")` or whitespace-only: disable commit attribution

View File

@@ -37,72 +37,11 @@ mod windows_impl {
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::runner_client::spawn_runner_transport;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
use crate::token::convert_string_sid_to_sid;
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
/// Ensures the parent directory of a path exists before writing to it.
/// Walks upward from `start` to locate the git worktree root, following gitfile redirects.
fn find_git_root(start: &Path) -> Option<PathBuf> {
let mut cur = dunce::canonicalize(start).ok()?;
loop {
let marker = cur.join(".git");
if marker.is_dir() {
return Some(cur);
}
if marker.is_file() {
if let Ok(txt) = std::fs::read_to_string(&marker)
&& let Some(rest) = txt.trim().strip_prefix("gitdir:")
{
let gitdir = rest.trim();
let resolved = if Path::new(gitdir).is_absolute() {
PathBuf::from(gitdir)
} else {
cur.join(gitdir)
};
return resolved.parent().map(Path::to_path_buf).or(Some(cur));
}
return Some(cur);
}
let parent = cur.parent()?;
if parent == cur {
return None;
}
cur = parent.to_path_buf();
}
}
/// Creates the sandbox user's Codex home directory if it does not already exist.
fn ensure_codex_home_exists(p: &Path) -> Result<()> {
std::fs::create_dir_all(p)?;
Ok(())
}
/// Adds a git safe.directory entry to the environment when running inside a repository.
/// git will not otherwise allow the Sandbox user to run git commands on the repo directory
/// which is owned by the primary user.
fn inject_git_safe_directory(
env_map: &mut HashMap<String, String>,
cwd: &Path,
_logs_base_dir: Option<&Path>,
) {
if let Some(git_root) = find_git_root(cwd) {
let mut cfg_count: usize = env_map
.get("GIT_CONFIG_COUNT")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(0);
let git_path = git_root.to_string_lossy().replace("\\\\", "/");
env_map.insert(
format!("GIT_CONFIG_KEY_{cfg_count}"),
"safe.directory".to_string(),
);
env_map.insert(format!("GIT_CONFIG_VALUE_{cfg_count}"), git_path);
cfg_count += 1;
env_map.insert("GIT_CONFIG_COUNT".to_string(), cfg_count.to_string());
}
}
pub use crate::windows_impl::CaptureResult;
@@ -130,7 +69,7 @@ mod windows_impl {
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
inherit_path_env(&mut env_map);
inject_git_safe_directory(&mut env_map, cwd, None);
inject_git_safe_directory(&mut env_map, cwd);
// Use a temp-based log dir that the sandbox user can write.
let sandbox_base = codex_home.join(".sandbox");
ensure_codex_home_exists(&sandbox_base)?;

View File

@@ -8,28 +8,12 @@
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
/// Walk upward from `start` to locate the git worktree root (supports gitfile redirects).
fn find_git_root(start: &Path) -> Option<PathBuf> {
/// Walk upward from `start` to locate the git worktree root for `safe.directory`.
fn find_git_worktree_root_for_safe_directory(start: &Path) -> Option<std::path::PathBuf> {
let mut cur = dunce::canonicalize(start).ok()?;
loop {
let marker = cur.join(".git");
if marker.is_dir() {
return Some(cur);
}
if marker.is_file() {
if let Ok(txt) = std::fs::read_to_string(&marker)
&& let Some(rest) = txt.trim().strip_prefix("gitdir:")
{
let gitdir = rest.trim();
let resolved = if Path::new(gitdir).is_absolute() {
PathBuf::from(gitdir)
} else {
cur.join(gitdir)
};
return resolved.parent().map(Path::to_path_buf).or(Some(cur));
}
if cur.join(".git").exists() {
return Some(cur);
}
let parent = cur.parent()?;
@@ -50,7 +34,7 @@ pub fn ensure_codex_home_exists(p: &Path) -> Result<()> {
/// git will not otherwise allow the Sandbox user to run git commands on the repo directory
/// which is owned by the primary user.
pub fn inject_git_safe_directory(env_map: &mut HashMap<String, String>, cwd: &Path) {
if let Some(git_root) = find_git_root(cwd) {
if let Some(git_root) = find_git_worktree_root_for_safe_directory(cwd) {
let mut cfg_count: usize = env_map
.get("GIT_CONFIG_COUNT")
.and_then(|v| v.parse::<usize>().ok())
@@ -65,3 +49,68 @@ pub fn inject_git_safe_directory(env_map: &mut HashMap<String, String>, cwd: &Pa
env_map.insert("GIT_CONFIG_COUNT".to_string(), cfg_count.to_string());
}
}
#[cfg(test)]
mod tests {
use super::inject_git_safe_directory;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn safe_directory_value(path: &Path) -> String {
dunce::canonicalize(path)
.expect("canonicalize path")
.to_string_lossy()
.replace("\\\\", "/")
}
#[test]
fn injects_safe_directory_for_git_directory() {
let temp = TempDir::new().expect("tempdir");
let repo = temp.path().join("repo");
let nested = repo.join("nested");
fs::create_dir_all(repo.join(".git")).expect("create .git");
fs::create_dir_all(&nested).expect("create nested dir");
let mut env_map = HashMap::new();
inject_git_safe_directory(&mut env_map, &nested);
let expected = HashMap::from([
("GIT_CONFIG_COUNT".to_string(), "1".to_string()),
("GIT_CONFIG_KEY_0".to_string(), "safe.directory".to_string()),
(
"GIT_CONFIG_VALUE_0".to_string(),
safe_directory_value(&repo),
),
]);
assert_eq!(env_map, expected);
}
#[test]
fn injects_worktree_root_for_gitfile() {
let temp = TempDir::new().expect("tempdir");
let repo = temp.path().join("repo");
let nested = repo.join("nested");
fs::create_dir_all(&nested).expect("create nested dir");
fs::write(
repo.join(".git"),
"gitdir: C:/Users/example/repo/.git/worktrees/codex3\n",
)
.expect("write .git file");
let mut env_map = HashMap::new();
inject_git_safe_directory(&mut env_map, &nested);
let expected = HashMap::from([
("GIT_CONFIG_COUNT".to_string(), "1".to_string()),
("GIT_CONFIG_KEY_0".to_string(), "safe.directory".to_string()),
(
"GIT_CONFIG_VALUE_0".to_string(),
safe_directory_value(&repo),
),
]);
assert_eq!(env_map, expected);
}
}

View File

@@ -5,3 +5,24 @@ For basic configuration instructions, see [this documentation](https://developer
For advanced configuration instructions, see [this documentation](https://developers.openai.com/codex/config-advanced).
For a full configuration reference, see [this documentation](https://developers.openai.com/codex/config-reference).
## Commit attribution
Codex can add a [git trailer](https://git-scm.com/docs/git-interpret-trailers) to
generated commit messages so commits make Codex's involvement explicit. This
behavior is gated by the `codex_git_commit` feature flag; the top-level
`commit_attribution` setting is only used when that feature is enabled.
Add the following to `~/.codex/config.toml`:
```toml
commit_attribution = "Codex <noreply@openai.com>"
[features]
codex_git_commit = true
```
When enabled, Codex appends a `Co-authored-by:` trailer using the configured
attribution value. If `commit_attribution` is omitted, Codex uses
`Codex <noreply@openai.com>`. Set `commit_attribution = ""` to disable the
trailer while leaving the feature flag enabled.