Skip to content

Commit 2974f47

Browse files
authored
feat(mcp): support session scoped MCP injection (#363)
## Summary - inject user-selected MCP servers at conversation scope for ACP flows - prevent MCP rename-on-edit regressions and track managed agent process trees for cleanup - merge latest main updates including Codex sandbox sync support ## Testing - cargo check -p aionui-ai-agent --------- Co-authored-by: zk <>
1 parent b848ddf commit 2974f47

55 files changed

Lines changed: 2697 additions & 1325 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ wiremock = "0.6"
180180
# Random
181181
getrandom = "0.2"
182182
hex = "0.4"
183+
libc = "0.2"
183184

184185
# Shrink target/debug: minimal debuginfo, sidecar files, no incremental cache.
185186
[profile.dev]

crates/aionui-ai-agent/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ async-trait.workspace = true
88
aionui-auth.workspace = true
99
aionui-common.workspace = true
1010
aionui-db.workspace = true
11+
aionui-mcp.workspace = true
1112
aionui-runtime.workspace = true
1213
aionui-extension.workspace = true
1314
aionui-api-types.workspace = true
@@ -38,6 +39,7 @@ uuid.workspace = true
3839
agent-client-protocol = { version = "0.11.1", features = ["unstable_session_model", "unstable_session_close", "unstable_session_usage", "unstable_session_fork", "unstable_session_additional_directories", "unstable_session_resume"] }
3940
tokio-util = { version = "0.7", features = ["compat"] }
4041
rusqlite.workspace = true
42+
libc.workspace = true
4143

4244
[features]
4345
# Enables the `AgentInstance::Mock` variant used by downstream test harnesses

crates/aionui-ai-agent/src/capability/cli_process/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,21 @@ impl CliAgentProcess {
156156

157157
// Force kill
158158
warn!(pid = self.pid, "Grace period expired, sending SIGKILL");
159-
force_kill(self.pid)
159+
force_kill(self.pid)?;
160+
161+
// Wait for the exit monitor to observe process termination so callers
162+
// do not race a still-live child after force-kill returns.
163+
let mut rx = self.exit_rx.clone();
164+
tokio::time::timeout(Duration::from_secs(5), async {
165+
if rx.borrow().is_some() {
166+
return;
167+
}
168+
let _ = rx.changed().await;
169+
})
170+
.await
171+
.map_err(|_| AppError::Internal(format!("Process {} did not exit after force_kill", self.pid)))?;
172+
173+
Ok(())
160174
}
161175

162176
/// Check whether the subprocess is still running.

crates/aionui-ai-agent/src/capability/cli_process/stderr_monitor.rs

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,62 @@ use tracing::{debug, error};
44

55
/// Force-kill a process by PID, plus any descendants.
66
///
7-
/// Uses platform-native shell commands so we don't pull in a `libc`/`winapi`
8-
/// dependency just for this one call site:
9-
/// * Unix: `kill -9 <pid>`
7+
/// Uses platform-native shell commands:
8+
/// * Unix: `kill -9 -<pid>` to target the spawned process group
109
/// * Windows: `taskkill /F /T /PID <pid>` (`/T` walks the process tree —
1110
/// the ACP CLI typically spawns a node/bun child that must die with it)
1211
///
1312
/// If the process has already exited, this is a no-op.
1413
pub(super) fn force_kill(pid: u32) -> Result<(), AppError> {
1514
#[cfg(unix)]
1615
{
17-
let result = std::process::Command::new("kill")
18-
.args(["-9", &pid.to_string()])
19-
.output();
16+
use std::io;
2017

21-
match result {
22-
Ok(output) if output.status.success() => {
23-
debug!(pid, "SIGKILL sent successfully");
24-
Ok(())
18+
fn kill_pid(target_pid: u32) -> Result<(), AppError> {
19+
let rc = unsafe { libc::kill(target_pid as i32, libc::SIGKILL) };
20+
if rc == 0 {
21+
debug!(pid = target_pid, "Direct SIGKILL sent successfully");
22+
return Ok(());
2523
}
26-
Ok(_output) => {
27-
// Non-zero exit likely means process already exited — acceptable
28-
debug!(pid, "Process already exited before SIGKILL");
24+
25+
let err = io::Error::last_os_error();
26+
if err.raw_os_error() == Some(libc::ESRCH) {
27+
debug!(pid = target_pid, "Process already exited before SIGKILL");
2928
Ok(())
29+
} else {
30+
error!(pid = target_pid, error = %err, "Direct SIGKILL failed");
31+
Err(AppError::Internal(format!(
32+
"Failed to kill process {target_pid}: {err}"
33+
)))
3034
}
31-
Err(e) => {
32-
error!(pid, error = %e, "Failed to execute kill command");
33-
Err(AppError::Internal(format!("Failed to kill process {pid}: {e}")))
35+
}
36+
37+
let pgid = unsafe { libc::getpgid(pid as i32) };
38+
if pgid == -1 {
39+
let err = io::Error::last_os_error();
40+
if err.raw_os_error() == Some(libc::ESRCH) {
41+
debug!(pid, "Process already exited before resolving process group");
42+
return Ok(());
43+
}
44+
45+
error!(pid, error = %err, "Failed to resolve process group");
46+
return kill_pid(pid);
47+
}
48+
49+
let rc = unsafe { libc::kill(-pgid, libc::SIGKILL) };
50+
if rc == 0 {
51+
debug!(pid, process_group = pgid, "SIGKILL sent successfully");
52+
Ok(())
53+
} else {
54+
let err = io::Error::last_os_error();
55+
if err.raw_os_error() == Some(libc::ESRCH) {
56+
debug!(pid, process_group = pgid, "Process group already exited before SIGKILL");
57+
kill_pid(pid)
58+
} else {
59+
error!(pid, process_group = pgid, error = %err, "Failed to send SIGKILL to process group");
60+
Err(AppError::Internal(format!(
61+
"Failed to kill process group {pgid}: {err}"
62+
)))
3463
}
3564
}
3665
}
@@ -79,6 +108,8 @@ pub(super) fn force_kill(pid: u32) -> Result<(), AppError> {
79108
#[cfg(test)]
80109
mod force_kill_tests {
81110
use super::force_kill;
111+
#[cfg(unix)]
112+
use std::os::unix::process::CommandExt;
82113
use std::process::Command;
83114
use std::time::{Duration, Instant};
84115

@@ -93,10 +124,11 @@ mod force_kill_tests {
93124
.spawn()
94125
.expect("spawn powershell sleep")
95126
} else {
96-
Command::new("sh")
97-
.args(["-c", "sleep 60"])
98-
.spawn()
99-
.expect("spawn sleep")
127+
let mut command = Command::new("sh");
128+
command.args(["-c", "sleep 60"]);
129+
#[cfg(unix)]
130+
command.process_group(0);
131+
command.spawn().expect("spawn sleep")
100132
}
101133
}
102134

0 commit comments

Comments
 (0)