Propagate exec host through app-server items

This commit is contained in:
Nathan Liebrecht
2026-04-10 14:30:29 -07:00
parent a80238d34f
commit a9fed68af5
27 changed files with 243 additions and 11 deletions
@@ -2865,6 +2865,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -13127,6 +13127,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -10982,6 +10982,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -684,6 +684,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -684,6 +684,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -827,6 +827,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1341,6 +1341,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1099,6 +1099,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1099,6 +1099,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1099,6 +1099,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1341,6 +1341,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1099,6 +1099,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1341,6 +1341,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1099,6 +1099,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -1099,6 +1099,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -827,6 +827,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -827,6 +827,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -827,6 +827,14 @@
"null"
]
},
"hostId": {
"default": null,
"description": "The configured exec host, when the command ran outside the local host.",
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
@@ -34,7 +34,11 @@ cwd: string,
/**
* Identifier for the underlying PTY process (when available).
*/
processId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus,
processId: string | null,
/**
* The configured exec host, when the command ran outside the local host.
*/
hostId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus,
/**
* A best-effort parsing of the command to understand the action(s) it will perform.
* This returns a list of CommandAction objects because a single shell command may
@@ -71,6 +71,7 @@ pub fn build_command_execution_approval_request_item(
command: shlex_join(&payload.command),
cwd: payload.cwd.clone(),
process_id: None,
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
command_actions: payload
@@ -91,6 +92,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
command: shlex_join(&payload.command),
cwd: payload.cwd.clone(),
process_id: payload.process_id.clone(),
host_id: payload.host_id.clone(),
source: payload.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions: payload
@@ -118,6 +120,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
command: shlex_join(&payload.command),
cwd: payload.cwd.clone(),
process_id: payload.process_id.clone(),
host_id: payload.host_id.clone(),
source: payload.source.into(),
status: (&payload.status).into(),
command_actions: payload
@@ -151,6 +154,7 @@ pub fn build_item_from_guardian_event(
command,
cwd: cwd.clone(),
process_id: None,
host_id: None,
source: CommandExecutionSource::Agent,
status,
command_actions,
@@ -183,6 +187,7 @@ pub fn build_item_from_guardian_event(
command,
cwd: cwd.clone(),
process_id: None,
host_id: None,
source: CommandExecutionSource::Agent,
status,
command_actions,
@@ -1837,6 +1837,7 @@ mod tests {
command: "echo 'hello world'".into(),
cwd: PathBuf::from("/tmp"),
process_id: Some("pid-1".into()),
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
@@ -2050,6 +2051,7 @@ mod tests {
command: "ls".into(),
cwd: PathBuf::from("/tmp"),
process_id: Some("pid-2".into()),
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Declined,
command_actions: vec![CommandAction::Unknown {
@@ -2135,6 +2137,7 @@ mod tests {
command: "rm -rf /tmp/guardian".into(),
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Declined,
command_actions: vec![CommandAction::Unknown {
@@ -2194,6 +2197,7 @@ mod tests {
command: "/bin/rm -f /tmp/file.sqlite".into(),
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
command_actions: vec![CommandAction::Unknown {
@@ -2284,6 +2288,7 @@ mod tests {
command: "echo done".into(),
cwd: PathBuf::from("/tmp"),
process_id: Some("pid-42".into()),
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
@@ -4409,6 +4409,9 @@ pub enum ThreadItem {
cwd: PathBuf,
/// Identifier for the underlying PTY process (when available).
process_id: Option<String>,
/// The configured exec host, when the command ran outside the local host.
#[serde(default)]
host_id: Option<String>,
#[serde(default)]
source: CommandExecutionSource,
status: CommandExecutionStatus,
@@ -133,7 +133,7 @@ fn read_file_bytes(path: &Path) -> Result<Vec<u8>> {
// fixture test is platform-independent.
let text = String::from_utf8(bytes)
.with_context(|| format!("expected UTF-8 TypeScript in {}", path.display()))?;
let text = text.replace("\r\n", "\n").replace('\r', "\n");
let text = normalize_typescript_fixture_text(&text);
// Fixture comparisons care about schema content, not whether the generator
// re-prepended the standard banner to every TypeScript file.
let text = text
@@ -276,10 +276,7 @@ fn collect_typescript_fixture_file<T: TS + 'static + ?Sized>(
let contents = T::export_to_string().context("export TypeScript fixture content")?;
let output_path = normalize_relative_fixture_path(&output_path);
files.insert(
output_path,
contents.replace("\r\n", "\n").replace('\r', "\n"),
);
files.insert(output_path, normalize_typescript_fixture_text(&contents));
let mut visitor = TypeScriptFixtureCollector {
files,
@@ -294,6 +291,20 @@ fn collect_typescript_fixture_file<T: TS + 'static + ?Sized>(
Ok(())
}
fn normalize_typescript_fixture_text(text: &str) -> String {
let text = text.replace("\r\n", "\n").replace('\r', "\n");
let mut normalized = String::with_capacity(text.len());
for line in text.split_inclusive('\n') {
if let Some(line) = line.strip_suffix('\n') {
normalized.push_str(line.trim_end_matches(|ch| ch == ' ' || ch == '\t'));
normalized.push('\n');
} else {
normalized.push_str(line.trim_end_matches(|ch| ch == ' ' || ch == '\t'));
}
}
normalized
}
fn normalize_relative_fixture_path(path: &Path) -> PathBuf {
path.components().collect()
}
@@ -1618,6 +1618,7 @@ pub(crate) async fn apply_bespoke_event_handling(
let command = shlex_join(&exec_command_begin_event.command);
let cwd = exec_command_begin_event.cwd;
let process_id = exec_command_begin_event.process_id;
let host_id = exec_command_begin_event.host_id;
let first_start = {
let mut state = thread_state.lock().await;
state
@@ -1631,6 +1632,7 @@ pub(crate) async fn apply_bespoke_event_handling(
command,
cwd,
process_id,
host_id,
source: exec_command_begin_event.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions,
@@ -2002,6 +2004,7 @@ async fn start_command_execution_item(
command,
cwd,
process_id: None,
host_id: None,
source,
status: CommandExecutionStatus::InProgress,
command_actions,
@@ -2046,6 +2049,7 @@ async fn complete_command_execution_item(
command,
cwd,
process_id,
host_id: None,
source,
status,
command_actions,
@@ -3228,6 +3232,7 @@ mod tests {
command: completion_item.command.clone(),
cwd: completion_item.cwd.clone(),
process_id: None,
host_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
command_actions: completion_item.command_actions.clone(),
+8 -2
View File
@@ -900,6 +900,7 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option<V
command,
cwd,
process_id,
host_id,
source,
command_actions,
..
@@ -922,7 +923,7 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option<V
.map(codex_app_server_protocol::CommandAction::into_core)
.collect(),
source: source.to_core(),
host_id: None,
host_id: host_id.clone(),
interaction_input: None,
}),
}])
@@ -935,6 +936,7 @@ fn command_execution_completed_event(turn_id: &str, item: &ThreadItem) -> Option
command,
cwd,
process_id,
host_id,
source,
status,
command_actions,
@@ -983,7 +985,7 @@ fn command_execution_completed_event(turn_id: &str, item: &ThreadItem) -> Option
.map(codex_app_server_protocol::CommandAction::into_core)
.collect(),
source: source.to_core(),
host_id: None,
host_id: host_id.clone(),
interaction_input: None,
stdout: String::new(),
stderr: String::new(),
@@ -1183,6 +1185,7 @@ mod tests {
command: "printf 'hello world\\n'".to_string(),
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: CommandExecutionSource::UserShell,
status: CommandExecutionStatus::InProgress,
command_actions: vec![CommandAction::Unknown {
@@ -1239,6 +1242,7 @@ mod tests {
command: "printf 'hello world\\n'".to_string(),
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: CommandExecutionSource::UserShell,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
@@ -1276,6 +1280,7 @@ mod tests {
command: r#"C:\Program Files\Git\bin\bash.exe -lc "echo hi""#.to_string(),
cwd: PathBuf::from("C:\\repo"),
process_id: None,
host_id: None,
source: CommandExecutionSource::UserShell,
status: CommandExecutionStatus::InProgress,
command_actions: vec![],
@@ -1324,6 +1329,7 @@ mod tests {
command: "printf 'hello world\\n'".to_string(),
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: CommandExecutionSource::UserShell,
status: CommandExecutionStatus::Completed,
command_actions: vec![CommandAction::Unknown {
+5 -3
View File
@@ -6195,6 +6195,7 @@ impl ChatWidget {
command,
cwd,
process_id,
host_id,
source,
status,
command_actions,
@@ -6217,7 +6218,7 @@ impl ChatWidget {
.map(codex_app_server_protocol::CommandAction::into_core)
.collect(),
source: source.to_core(),
host_id: None,
host_id,
interaction_input: None,
});
} else {
@@ -6233,7 +6234,7 @@ impl ChatWidget {
.map(codex_app_server_protocol::CommandAction::into_core)
.collect(),
source: source.to_core(),
host_id: None,
host_id,
interaction_input: None,
stdout: String::new(),
stderr: String::new(),
@@ -6790,6 +6791,7 @@ impl ChatWidget {
command,
cwd,
process_id,
host_id,
source,
command_actions,
..
@@ -6805,7 +6807,7 @@ impl ChatWidget {
.map(codex_app_server_protocol::CommandAction::into_core)
.collect(),
source: source.to_core(),
host_id: None,
host_id,
interaction_input: None,
});
}
@@ -193,6 +193,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
command: command.clone(),
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: AppServerCommandExecutionSource::UserShell,
status: AppServerCommandExecutionStatus::InProgress,
command_actions: vec![AppServerCommandAction::Unknown {
@@ -214,6 +215,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
command,
cwd: PathBuf::from("/tmp"),
process_id: None,
host_id: None,
source: AppServerCommandExecutionSource::UserShell,
status: AppServerCommandExecutionStatus::Completed,
command_actions: vec![AppServerCommandAction::Unknown {
@@ -240,6 +242,51 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
);
}
#[tokio::test]
async fn live_app_server_command_execution_shows_remote_host() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.on_task_started();
let script = "echo remote";
let command =
shlex::try_join(["/bin/zsh", "-lc", script]).expect("round-trippable shell wrapper");
chat.handle_server_notification(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item: AppServerThreadItem::CommandExecution {
id: "cmd-remote".to_string(),
command,
cwd: PathBuf::from("/Users/nlieb"),
process_id: Some("proc-remote".to_string()),
host_id: Some("mac".to_string()),
source: AppServerCommandExecutionSource::UnifiedExecStartup,
status: AppServerCommandExecutionStatus::Completed,
command_actions: vec![AppServerCommandAction::Unknown {
command: script.to_string(),
}],
aggregated_output: Some("remote\n".to_string()),
exit_code: Some(0),
duration_ms: Some(5),
},
}),
/*replay_kind*/ None,
);
let cells = drain_insert_history(&mut rx);
assert_eq!(
cells.len(),
1,
"expected one completed command history cell"
);
let blob = lines_to_single_string(cells.first().expect("command cell"));
assert!(
blob.contains("• Ran on mac echo remote"),
"expected remote host in app-server command row: {blob:?}"
);
}
#[test]
fn app_server_patch_changes_to_core_preserves_diffs() {
let changes = app_server_patch_changes_to_core(vec![FileUpdateChange {