add(core): Add GitInfo to EnvironmentContext

This commit is contained in:
Dylan Hurd
2025-10-26 21:54:40 -07:00
parent a55b0c4bcc
commit 15cd23845c
3 changed files with 156 additions and 4 deletions

View File

@@ -65,6 +65,7 @@ use crate::error::CodexErr;
use crate::error::Result as CodexResult;
#[cfg(test)]
use crate::exec::StreamOutput;
use crate::git_info::collect_git_info;
// Removed: legacy executor wiring replaced by ToolOrchestrator flows.
// legacy normalize_exec_result no longer used after orchestrator migration
use crate::mcp::auth::compute_auth_statuses;
@@ -127,6 +128,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::GitInfo;
use codex_protocol::protocol::InitialHistory;
use codex_protocol::user_input::UserInput;
@@ -168,6 +170,8 @@ impl Codex {
let config = Arc::new(config);
let git_info = collect_git_info(&config.cwd).await;
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
model: config.model.clone(),
@@ -178,6 +182,7 @@ impl Codex {
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
git_info,
original_config_do_not_use: Arc::clone(&config),
};
@@ -263,6 +268,7 @@ pub(crate) struct TurnContext {
/// the model as well as sandbox policies are resolved against this path
/// instead of `std::env::current_dir()`.
pub(crate) cwd: PathBuf,
pub(crate) git_info: Option<GitInfo>,
pub(crate) base_instructions: Option<String>,
pub(crate) user_instructions: Option<String>,
pub(crate) approval_policy: AskForApproval,
@@ -313,6 +319,9 @@ pub(crate) struct SessionConfiguration {
/// operate deterministically.
cwd: PathBuf,
/// Git metadata for the current working directory, if available.
git_info: Option<GitInfo>,
// TODO(pakrym): Remove config from here
original_config_do_not_use: Arc<Config>,
}
@@ -338,6 +347,9 @@ impl SessionConfiguration {
if let Some(cwd) = updates.cwd.clone() {
next_configuration.cwd = cwd;
}
if let Some(git_info) = updates.git_info.clone() {
next_configuration.git_info = git_info;
}
next_configuration
}
}
@@ -345,6 +357,7 @@ impl SessionConfiguration {
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) cwd: Option<PathBuf>,
pub(crate) git_info: Option<Option<GitInfo>>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) sandbox_policy: Option<SandboxPolicy>,
pub(crate) model: Option<String>,
@@ -398,6 +411,7 @@ impl Session {
sub_id,
client,
cwd: session_configuration.cwd.clone(),
git_info: session_configuration.git_info.clone(),
base_instructions: session_configuration.base_instructions.clone(),
user_instructions: session_configuration.user_instructions.clone(),
approval_policy: session_configuration.approval_policy,
@@ -962,6 +976,7 @@ impl Session {
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
Some(self.user_shell().clone()),
turn_context.git_info.clone(),
)));
items
}
@@ -1190,7 +1205,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
effort,
summary,
} => {
let updates = SessionSettingsUpdate {
let mut updates = SessionSettingsUpdate {
cwd,
approval_policy,
sandbox_policy,
@@ -1199,11 +1214,14 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
reasoning_summary: summary,
..Default::default()
};
if let Some(cwd) = updates.cwd.clone() {
updates.git_info = Some(collect_git_info(&cwd).await);
}
sess.update_settings(updates).await;
}
Op::UserInput { .. } | Op::UserTurn { .. } => {
let (items, updates) = match sub.op {
let (items, mut updates) = match sub.op {
Op::UserTurn {
cwd,
approval_policy,
@@ -1217,6 +1235,7 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
items,
SessionSettingsUpdate {
cwd: Some(cwd),
git_info: None,
approval_policy: Some(approval_policy),
sandbox_policy: Some(sandbox_policy),
model: Some(model),
@@ -1228,6 +1247,9 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
Op::UserInput { items } => (items, SessionSettingsUpdate::default()),
_ => unreachable!(),
};
if let Some(cwd) = updates.cwd.clone() {
updates.git_info = Some(collect_git_info(&cwd).await);
}
let current_context = sess.new_turn_with_sub_id(sub.id.clone(), updates).await;
current_context
.client
@@ -1515,6 +1537,7 @@ async fn spawn_review_thread(
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
cwd: parent_turn_context.cwd.clone(),
git_info: parent_turn_context.git_info.clone(),
is_review_mode: true,
final_output_json_schema: None,
codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(),
@@ -2601,6 +2624,7 @@ mod tests {
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
git_info: None,
original_config_do_not_use: Arc::clone(&config),
};
@@ -2669,6 +2693,7 @@ mod tests {
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
git_info: None,
original_config_do_not_use: Arc::clone(&config),
};

View File

@@ -11,6 +11,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
use codex_protocol::protocol::GitInfo;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, DeriveDisplay)]
@@ -29,6 +30,8 @@ pub(crate) struct EnvironmentContext {
pub network_access: Option<NetworkAccess>,
pub writable_roots: Option<Vec<PathBuf>>,
pub shell: Option<Shell>,
pub git_info: Option<GitInfo>,
pub git_info_cleared: bool,
}
impl EnvironmentContext {
@@ -37,6 +40,7 @@ impl EnvironmentContext {
approval_policy: Option<AskForApproval>,
sandbox_policy: Option<SandboxPolicy>,
shell: Option<Shell>,
git_info: Option<GitInfo>,
) -> Self {
Self {
cwd,
@@ -70,6 +74,8 @@ impl EnvironmentContext {
_ => None,
},
shell,
git_info,
git_info_cleared: false,
}
}
@@ -83,8 +89,10 @@ impl EnvironmentContext {
sandbox_mode,
network_access,
writable_roots,
git_info,
// should compare all fields except shell
shell: _,
git_info_cleared: _,
} = other;
self.cwd == *cwd
@@ -92,6 +100,7 @@ impl EnvironmentContext {
&& self.sandbox_mode == *sandbox_mode
&& self.network_access == *network_access
&& self.writable_roots == *writable_roots
&& self.git_info == *git_info
}
pub fn diff(before: &TurnContext, after: &TurnContext) -> Self {
@@ -110,7 +119,22 @@ impl EnvironmentContext {
} else {
None
};
EnvironmentContext::new(cwd, approval_policy, sandbox_policy, None)
let git_info_changed = before.git_info != after.git_info;
let mut context = EnvironmentContext::new(
cwd,
approval_policy,
sandbox_policy,
None,
if git_info_changed {
after.git_info.clone()
} else {
None
},
);
if git_info_changed && before.git_info.is_some() && after.git_info.is_none() {
context.git_info_cleared = true;
}
context
}
}
@@ -122,6 +146,7 @@ impl From<&TurnContext> for EnvironmentContext {
Some(turn_context.sandbox_policy.clone()),
// Shell is not configurable from turn to turn
None,
turn_context.git_info.clone(),
)
}
}
@@ -139,6 +164,7 @@ impl EnvironmentContext {
/// <writable_roots>...</writable_roots>
/// <network_access>...</network_access>
/// <shell>...</shell>
/// <git_info>...</git_info>
/// </environment_context>
/// ```
pub fn serialize_to_xml(self) -> String {
@@ -174,6 +200,23 @@ impl EnvironmentContext {
{
lines.push(format!(" <shell>{shell_name}</shell>"));
}
if let Some(git_info) = self.git_info {
lines.push(" <git_info>".to_string());
if let Some(commit_hash) = git_info.commit_hash {
lines.push(format!(" <commit_hash>{commit_hash}</commit_hash>"));
}
if let Some(branch) = git_info.branch {
lines.push(format!(" <branch>{branch}</branch>"));
}
if let Some(repository_url) = git_info.repository_url {
lines.push(format!(
" <repository_url>{repository_url}</repository_url>"
));
}
lines.push(" </git_info>".to_string());
} else if self.git_info_cleared {
lines.push(" <git_info />".to_string());
}
lines.push(ENVIRONMENT_CONTEXT_CLOSE_TAG.to_string());
lines.join("\n")
}
@@ -215,6 +258,7 @@ mod tests {
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo", "/tmp"], false)),
None,
None,
);
let expected = r#"<environment_context>
@@ -238,6 +282,7 @@ mod tests {
Some(AskForApproval::Never),
Some(SandboxPolicy::ReadOnly),
None,
None,
);
let expected = r#"<environment_context>
@@ -256,6 +301,7 @@ mod tests {
Some(AskForApproval::OnFailure),
Some(SandboxPolicy::DangerFullAccess),
None,
None,
);
let expected = r#"<environment_context>
@@ -267,6 +313,48 @@ mod tests {
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_environment_context_with_git_info() {
let git_info = GitInfo {
commit_hash: Some("abc123".to_string()),
branch: Some("main".to_string()),
repository_url: Some("https://example.com/repo.git".to_string()),
};
let context = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::ReadOnly),
None,
Some(git_info),
);
let expected = r#"<environment_context>
<cwd>/repo</cwd>
<approval_policy>on-request</approval_policy>
<sandbox_mode>read-only</sandbox_mode>
<network_access>restricted</network_access>
<git_info>
<commit_hash>abc123</commit_hash>
<branch>main</branch>
<repository_url>https://example.com/repo.git</repository_url>
</git_info>
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn serialize_environment_context_with_cleared_git_info() {
let mut context = EnvironmentContext::new(None, None, None, None, None);
context.git_info_cleared = true;
let expected = r#"<environment_context>
<git_info />
</environment_context>"#;
assert_eq!(context.serialize_to_xml(), expected);
}
#[test]
fn equals_except_shell_compares_approval_policy() {
// Approval policy
@@ -275,12 +363,14 @@ mod tests {
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo"], false)),
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::Never),
Some(workspace_write_policy(vec!["/repo"], true)),
None,
None,
);
assert!(!context1.equals_except_shell(&context2));
}
@@ -292,12 +382,14 @@ mod tests {
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::new_read_only_policy()),
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(SandboxPolicy::new_workspace_write_policy()),
None,
None,
);
assert!(!context1.equals_except_shell(&context2));
@@ -310,12 +402,45 @@ mod tests {
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo", "/tmp", "/var"], false)),
None,
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo", "/tmp"], true)),
None,
None,
);
assert!(!context1.equals_except_shell(&context2));
}
#[test]
fn equals_except_shell_compares_git_info() {
let git_info1 = Some(GitInfo {
commit_hash: Some("abc".to_string()),
branch: Some("main".to_string()),
repository_url: None,
});
let git_info2 = Some(GitInfo {
commit_hash: Some("def".to_string()),
branch: Some("feature".to_string()),
repository_url: None,
});
let context1 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo"], false)),
None,
git_info1,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
Some(AskForApproval::OnRequest),
Some(workspace_write_policy(vec!["/repo"], false)),
None,
git_info2,
);
assert!(!context1.equals_except_shell(&context2));
@@ -331,6 +456,7 @@ mod tests {
shell_path: "/bin/bash".into(),
bashrc_path: "/home/user/.bashrc".into(),
})),
None,
);
let context2 = EnvironmentContext::new(
Some(PathBuf::from("/repo")),
@@ -340,6 +466,7 @@ mod tests {
shell_path: "/bin/zsh".into(),
zshrc_path: "/home/user/.zshrc".into(),
})),
None,
);
assert!(context1.equals_except_shell(&context2));

View File

@@ -1010,7 +1010,7 @@ pub struct RolloutLine {
pub item: RolloutItem,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
pub struct GitInfo {
/// Current commit hash (SHA)
#[serde(skip_serializing_if = "Option::is_none")]