Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions codi-rs/src/orchestrate/child_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub struct ChildAgent {
workspace: WorkspaceInfo,
/// Auto-approved tools from handshake.
auto_approve: Vec<String>,
/// Dangerous patterns from handshake.
dangerous_patterns: Vec<String>,
/// Timeout from handshake.
timeout_ms: u64,
}
Expand Down Expand Up @@ -107,13 +109,15 @@ impl ChildAgent {

let ipc = Arc::new(Mutex::new(ipc));
let auto_approve = ack.auto_approve.clone();
let dangerous_patterns = ack.dangerous_patterns.clone();

// Create agent
let mut child_agent = Self {
ipc: Arc::clone(&ipc),
config,
workspace,
auto_approve,
dangerous_patterns,
timeout_ms: ack.timeout_ms,
};

Expand Down Expand Up @@ -183,7 +187,7 @@ impl ChildAgent {
let callbacks = AgentCallbacks {
on_confirm: Some(Arc::new(move |confirmation: ToolConfirmation| {
// Check auto-approve list
if auto_approve.contains(&confirmation.tool_name) {
if !confirmation.is_dangerous && auto_approve.contains(&confirmation.tool_name) {
return ConfirmationResult::Approve;
}

Expand Down Expand Up @@ -241,7 +245,7 @@ impl ChildAgent {
extract_tools_from_text: true,
auto_approve_all: false,
auto_approve_tools: self.auto_approve.clone(),
dangerous_patterns: Vec::new(),
dangerous_patterns: self.dangerous_patterns.clone(),
};

let mut agent = Agent::new(AgentOptions {
Expand Down
15 changes: 14 additions & 1 deletion codi-rs/src/orchestrate/commander.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,21 @@ impl Commander {
.unwrap_or(300_000)
};

let dangerous_patterns = {
let workers = workers.read().await;
workers
.get(&worker_id)
.map(|w| w.config.dangerous_patterns.clone())
.unwrap_or_default()
};

// Send ack
let ack = CommanderMessage::handshake_ack(true, auto_approve, timeout_ms);
let ack = CommanderMessage::handshake_ack(
true,
auto_approve,
dangerous_patterns,
timeout_ms
);
if let Err(e) = self.server.send(&worker_id, &ack).await {
error!("Failed to send handshake ack: {}", e);
}
Expand Down
98 changes: 91 additions & 7 deletions codi-rs/src/orchestrate/ipc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ pub enum IpcClientError {
/// Handshake acknowledgment from commander.
#[derive(Debug, Clone)]
pub struct HandshakeAck {
/// Whether the handshake was accepted.
pub accepted: bool,
/// Tools that can be auto-approved.
pub auto_approve: Vec<String>,
/// Dangerous patterns for tool inputs.
pub dangerous_patterns: Vec<String>,
/// Timeout in milliseconds.
pub timeout_ms: u64,
/// Optional rejection reason.
pub reason: Option<String>,
}

/// Pending permission request.
Expand All @@ -81,6 +87,8 @@ pub struct IpcClient {
cancel_tx: Option<mpsc::Sender<()>>,
/// Whether we've been cancelled.
cancelled: Arc<Mutex<bool>>,
/// Latest handshake acknowledgement.
handshake_ack: Arc<Mutex<Option<HandshakeAck>>>,
}

impl IpcClient {
Expand All @@ -93,6 +101,7 @@ impl IpcClient {
pending_permissions: Arc::new(Mutex::new(HashMap::new())),
cancel_tx: None,
cancelled: Arc::new(Mutex::new(false)),
handshake_ack: Arc::new(Mutex::new(None)),
}
}

Expand All @@ -109,6 +118,8 @@ impl IpcClient {
let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
self.cancel_tx = Some(cancel_tx);

let handshake_ack = Arc::clone(&self.handshake_ack);

tokio::spawn(async move {
let mut reader = BufReader::new(read_half);
let mut line = String::new();
Expand All @@ -123,7 +134,12 @@ impl IpcClient {
}
Ok(_) => {
if let Ok(msg) = decode::<CommanderMessage>(&line) {
Self::handle_commander_message(msg, &pending, &cancelled).await;
Self::handle_commander_message(
msg,
&pending,
&cancelled,
&handshake_ack
).await;
}
line.clear();
}
Expand All @@ -150,8 +166,26 @@ impl IpcClient {
msg: CommanderMessage,
pending: &Arc<Mutex<HashMap<String, PendingPermission>>>,
cancelled: &Arc<Mutex<bool>>,
handshake_ack: &Arc<Mutex<Option<HandshakeAck>>>,
) {
match msg {
CommanderMessage::HandshakeAck {
accepted,
auto_approve,
dangerous_patterns,
timeout_ms,
reason,
..
} => {
let mut ack = handshake_ack.lock().await;
*ack = Some(HandshakeAck {
accepted,
auto_approve,
dangerous_patterns,
timeout_ms,
reason,
});
}
CommanderMessage::PermissionResponse { request_id, result, .. } => {
let mut pending = pending.lock().await;
if let Some(req) = pending.remove(&request_id) {
Expand Down Expand Up @@ -202,13 +236,63 @@ impl IpcClient {
writer.write_all(encoded.as_bytes()).await?;
writer.flush().await?;

// Wait for handshake ack (with timeout)
// Note: The actual ack comes through the reader task, but for simplicity
// we'll just return the config values
Ok(HandshakeAck {
auto_approve: config.auto_approve.clone(),
timeout_ms: config.timeout_ms,
let ack = self
.wait_for_handshake_ack(Duration::from_secs(2))
.await;

if let Some(ack) = ack {
if !ack.accepted {
return Err(IpcClientError::HandshakeFailed(
ack.reason.unwrap_or_else(|| "Handshake rejected".to_string())
));
}

// If commander didn't provide values, fall back to local config
let auto_approve = if ack.auto_approve.is_empty() {
config.auto_approve.clone()
} else {
ack.auto_approve
};
let dangerous_patterns = if ack.dangerous_patterns.is_empty() {
config.dangerous_patterns.clone()
} else {
ack.dangerous_patterns
};
let timeout_ms = if ack.timeout_ms == 0 { config.timeout_ms } else { ack.timeout_ms };

Ok(HandshakeAck {
accepted: true,
auto_approve,
dangerous_patterns,
timeout_ms,
reason: None,
})
} else {
warn!("Handshake ack not received; using local config defaults");
Ok(HandshakeAck {
accepted: true,
auto_approve: config.auto_approve.clone(),
dangerous_patterns: config.dangerous_patterns.clone(),
timeout_ms: config.timeout_ms,
reason: None,
})
}
}

async fn wait_for_handshake_ack(&self, timeout: Duration) -> Option<HandshakeAck> {
match tokio::time::timeout(timeout, async {
loop {
if let Some(ack) = self.handshake_ack.lock().await.take() {
return ack;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await
{
Ok(ack) => Some(ack),
Err(_) => None,
}
}

/// Request permission for a tool operation.
Expand Down
18 changes: 16 additions & 2 deletions codi-rs/src/orchestrate/ipc/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ pub enum CommanderMessage {
accepted: bool,
/// Tools to auto-approve.
auto_approve: Vec<String>,
/// Dangerous patterns for tool inputs.
dangerous_patterns: Vec<String>,
/// Timeout in milliseconds.
timeout_ms: u64,
/// Rejection reason (if not accepted).
Expand Down Expand Up @@ -377,12 +379,18 @@ pub enum PermissionResult {

impl CommanderMessage {
/// Create a handshake acknowledgment.
pub fn handshake_ack(accepted: bool, auto_approve: Vec<String>, timeout_ms: u64) -> Self {
pub fn handshake_ack(
accepted: bool,
auto_approve: Vec<String>,
dangerous_patterns: Vec<String>,
timeout_ms: u64
) -> Self {
Self::HandshakeAck {
id: generate_message_id(),
timestamp: now(),
accepted,
auto_approve,
dangerous_patterns,
timeout_ms,
reason: None,
}
Expand All @@ -395,6 +403,7 @@ impl CommanderMessage {
timestamp: now(),
accepted: false,
auto_approve: Vec::new(),
dangerous_patterns: Vec::new(),
timeout_ms: 0,
reason: Some(reason.into()),
}
Expand Down Expand Up @@ -577,7 +586,12 @@ mod tests {

#[test]
fn test_commander_messages() {
let ack = CommanderMessage::handshake_ack(true, vec!["read_file".to_string()], 60000);
let ack = CommanderMessage::handshake_ack(
true,
vec!["read_file".to_string()],
vec![],
60000
);
assert!(ack.is_handshake_ack());

let cancel = CommanderMessage::cancel(Some("User requested".to_string()));
Expand Down
10 changes: 10 additions & 0 deletions codi-rs/src/orchestrate/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub struct WorkerConfig {
/// Tools to auto-approve without permission requests.
#[serde(default)]
pub auto_approve: Vec<String>,
/// Dangerous patterns for tool inputs (passed to workers).
#[serde(default)]
pub dangerous_patterns: Vec<String>,
/// Maximum iterations before stopping.
#[serde(default = "default_max_iterations")]
pub max_iterations: u32,
Expand All @@ -64,6 +67,7 @@ impl WorkerConfig {
model: None,
provider: None,
auto_approve: Vec::new(),
dangerous_patterns: Vec::new(),
max_iterations: default_max_iterations(),
timeout_ms: default_timeout_ms(),
}
Expand All @@ -87,6 +91,12 @@ impl WorkerConfig {
self
}

/// Set dangerous patterns for tool inputs.
pub fn with_dangerous_patterns(mut self, patterns: Vec<String>) -> Self {
self.dangerous_patterns = patterns;
self
}

/// Check if a tool should be auto-approved.
pub fn should_auto_approve(&self, tool_name: &str) -> bool {
self.auto_approve.iter().any(|t| t == tool_name)
Expand Down
7 changes: 6 additions & 1 deletion codi-rs/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,12 @@ impl App {
// Generate worker ID from branch
let worker_id = branch.replace('/', "-");

let config = WorkerConfig::new(&worker_id, branch, task);
let mut config = WorkerConfig::new(&worker_id, branch, task);
if let Some(ref resolved) = self.config {
config = config
.with_auto_approve(resolved.auto_approve.clone())
.with_dangerous_patterns(resolved.dangerous_patterns.clone());
}

commander
.spawn_worker(config)
Expand Down
Loading