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
2 changes: 1 addition & 1 deletion codi-rs/src/orchestrate/child_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//!
//! # Lifecycle
//!
//! 1. Connect to commander's IPC socket
//! 1. Connect to commander's IPC endpoint
//! 2. Perform handshake with worker config
//! 3. Execute task with agent loop
//! 4. Request permissions via IPC when needed
Expand Down
4 changes: 2 additions & 2 deletions codi-rs/src/orchestrate/commander.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
//! ┌─────────────────────────────────────────────────────────────┐
//! │ Commander │
//! │ ┌──────────────────────────────────────────────────────┐ │
//! │ │ IPC Server (Unix domain socket) │ │
//! │ │ ~/.codi/orchestrator.sock │ │
//! │ │ IPC Server (socket/pipe) │ │
//! │ │ ~/.codi/orchestrator.sock or \\\\.\\pipe\\... │ │
//! │ └──────────────────┬───────────────────────────────────┘ │
//! │ │ │
//! │ ┌──────────────────┴───────────────────────────────────┐ │
Expand Down
14 changes: 7 additions & 7 deletions codi-rs/src/orchestrate/ipc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! IPC client for worker agents.
//!
//! The client connects to the commander's Unix domain socket and handles
//! The client connects to the commander's IPC endpoint and handles
//! bidirectional communication for permission requests and status updates.

use std::collections::HashMap;
Expand All @@ -12,7 +12,6 @@ use std::sync::Arc;
use std::time::Duration;

use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use tokio::sync::{mpsc, oneshot, Mutex};
use tracing::{debug, error, info, warn};

Expand All @@ -22,6 +21,7 @@ use crate::types::TokenUsage;
use super::protocol::{
decode, encode, CommanderMessage, PermissionResult, WorkerMessage,
};
use super::transport::{self, IpcStream};
use super::super::types::{WorkerConfig, WorkerResult, WorkerStatus, WorkspaceInfo};

/// Error type for IPC client operations.
Expand Down Expand Up @@ -75,12 +75,12 @@ struct PendingPermission {

/// IPC client for worker-commander communication.
pub struct IpcClient {
/// Path to the Unix socket.
/// Path to the IPC endpoint.
socket_path: PathBuf,
/// Worker ID.
worker_id: String,
/// Writer half of the socket.
writer: Option<tokio::io::WriteHalf<UnixStream>>,
/// Writer half of the stream.
writer: Option<tokio::io::WriteHalf<IpcStream>>,
/// Pending permission requests by request ID.
pending_permissions: Arc<Mutex<HashMap<String, PendingPermission>>>,
/// Channel for cancel signals.
Expand All @@ -105,9 +105,9 @@ impl IpcClient {
}
}

/// Connect to the commander's socket.
/// Connect to the commander's endpoint.
pub async fn connect(&mut self) -> Result<(), IpcClientError> {
let stream = UnixStream::connect(&self.socket_path).await?;
let stream = transport::connect(&self.socket_path).await?;
let (read_half, write_half) = tokio::io::split(stream);

self.writer = Some(write_half);
Expand Down
80 changes: 76 additions & 4 deletions codi-rs/src/orchestrate/ipc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@

//! IPC (Inter-Process Communication) module for commander-worker communication.
//!
//! This module provides Unix domain socket-based communication between the
//! commander (orchestrator) and worker (child agent) processes.
//! This module provides cross-platform IPC between the commander (orchestrator)
//! and worker (child agent) processes.
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────┐ ┌─────────────────┐
//! │ Commander │ │ Worker │
//! │ │ │ │
//! │ ┌───────────┐ │ Unix │ ┌───────────┐ │
//! │ │ Server │◄─┼──Socket──────┼──│ Client │ │
//! │ ┌───────────┐ │ Socket/ │ ┌───────────┐ │
//! │ │ Server │◄─┼──Pipe────────┼──│ Client │ │
//! │ └───────────┘ │ │ └───────────┘ │
//! └─────────────────┘ └─────────────────┘
//! ```
Expand All @@ -23,6 +23,10 @@
//! Messages are newline-delimited JSON (NDJSON). Each message is a complete
//! JSON object followed by a newline character.
//!
//! Transport:
//! - Unix: domain sockets
//! - Windows: named pipes
//!
//! ## Worker → Commander Messages
//!
//! - `handshake` - Initial connection from worker
Expand All @@ -44,6 +48,7 @@
pub mod protocol;
pub mod server;
pub mod client;
pub mod transport;

pub use protocol::{
WorkerMessage, CommanderMessage, PermissionResult,
Expand All @@ -52,3 +57,70 @@ pub use protocol::{
};
pub use server::IpcServer;
pub use client::IpcClient;

#[cfg(test)]
mod tests {
use super::{CommanderMessage, IpcClient, IpcServer, WorkerMessage};
use crate::orchestrate::types::{WorkerConfig, WorkspaceInfo};
use std::path::PathBuf;
use std::sync::Arc;

#[cfg(windows)]
#[tokio::test]
async fn test_named_pipe_handshake_roundtrip() {
let pipe_name = format!(r"\\.\pipe\codi-ipc-handshake-{}", uuid::Uuid::new_v4());
let socket_path = PathBuf::from(pipe_name);

let mut server = IpcServer::new(&socket_path);
server.start().await.expect("server start failed");

let mut rx = server.take_receiver().expect("receiver already taken");
let server = Arc::new(server);

let accept_server = Arc::clone(&server);
let accept_task = tokio::spawn(async move {
accept_server.accept().await.expect("accept failed")
});

let ack_server = Arc::clone(&server);
let ack_task = tokio::spawn(async move {
let (worker_id, msg) = rx.recv().await.expect("handshake missing");
assert_eq!(worker_id, "worker-1");
assert!(matches!(msg, WorkerMessage::Handshake { .. }));

let ack = CommanderMessage::handshake_ack(
true,
vec!["read_file".to_string()],
vec!["rm -rf".to_string()],
1_234,
);

ack_server
.send(&worker_id, &ack)
.await
.expect("ack send failed");
});

let mut client = IpcClient::new(&socket_path, "worker-1");
client.connect().await.expect("client connect failed");

let workspace = WorkspaceInfo::GitWorktree {
path: PathBuf::from("."),
branch: "feat/test".to_string(),
base_branch: "main".to_string(),
};
let config = WorkerConfig::new("worker-1", "feat/test", "task");

let ack = client
.handshake(&config, &workspace)
.await
.expect("handshake failed");

assert_eq!(ack.auto_approve, vec!["read_file".to_string()]);
assert_eq!(ack.dangerous_patterns, vec!["rm -rf".to_string()]);
assert_eq!(ack.timeout_ms, 1_234);

accept_task.await.expect("accept task failed");
ack_task.await.expect("ack task failed");
}
}
2 changes: 1 addition & 1 deletion codi-rs/src/orchestrate/ipc/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! IPC protocol for commander-worker communication.
//!
//! Uses newline-delimited JSON over Unix domain sockets.
//! Uses newline-delimited JSON over a platform-specific IPC transport.

use serde::{Deserialize, Serialize};
use uuid::Uuid;
Expand Down
46 changes: 16 additions & 30 deletions codi-rs/src/orchestrate/ipc/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@

//! IPC server for the commander.
//!
//! The server listens on a Unix domain socket and handles connections
//! from worker processes.
//! The server listens on a platform-specific IPC transport and handles
//! connections from worker processes.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::{mpsc, Mutex, RwLock};
use tracing::{debug, error, info, warn};

use super::protocol::{
decode, encode, CommanderMessage, WorkerMessage,
};
use super::transport::{self, IpcListener, IpcStream};

/// Error type for IPC operations.
#[derive(Debug, thiserror::Error)]
Expand All @@ -43,18 +43,18 @@ pub enum IpcError {

/// A connected worker client.
struct ConnectedWorker {
/// Write half of the socket.
writer: tokio::io::WriteHalf<UnixStream>,
/// Write half of the stream.
writer: tokio::io::WriteHalf<IpcStream>,
/// Worker ID (stored for logging/diagnostics).
_worker_id: String,
}

/// IPC server for commander-worker communication.
pub struct IpcServer {
/// Path to the Unix socket.
/// Path to the IPC endpoint.
socket_path: PathBuf,
/// Listener (set after start).
listener: Option<UnixListener>,
listener: Option<IpcListener>,
/// Connected workers by ID.
workers: Arc<RwLock<HashMap<String, Arc<Mutex<ConnectedWorker>>>>>,
/// Channel for incoming messages (worker_id, message).
Expand All @@ -76,25 +76,14 @@ impl IpcServer {
}
}

/// Get the socket path.
/// Get the IPC endpoint path.
pub fn socket_path(&self) -> &Path {
&self.socket_path
}

/// Start the server.
pub async fn start(&mut self) -> Result<(), IpcError> {
// Remove existing socket file if present
if self.socket_path.exists() {
std::fs::remove_file(&self.socket_path)?;
}

// Create parent directory if needed
if let Some(parent) = self.socket_path.parent() {
std::fs::create_dir_all(parent)?;
}

// Bind the listener
let listener = UnixListener::bind(&self.socket_path)?;
let listener = transport::bind(&self.socket_path).await?;
info!("IPC server listening on {:?}", self.socket_path);
self.listener = Some(listener);

Expand All @@ -107,10 +96,7 @@ impl IpcServer {
let mut workers = self.workers.write().await;
workers.clear();

// Remove socket file
if self.socket_path.exists() {
std::fs::remove_file(&self.socket_path)?;
}
transport::cleanup(&self.socket_path)?;

self.listener = None;
info!("IPC server stopped");
Expand All @@ -131,7 +117,7 @@ impl IpcServer {
pub async fn accept(&self) -> Result<String, IpcError> {
let listener = self.listener.as_ref().ok_or(IpcError::NotStarted)?;

let (stream, _addr) = listener.accept().await?;
let stream = listener.accept().await?;
debug!("New connection accepted");

let (read_half, write_half) = tokio::io::split(stream);
Expand Down Expand Up @@ -177,7 +163,7 @@ impl IpcServer {

/// Background task to read messages from a worker.
async fn read_worker_messages(
mut reader: BufReader<tokio::io::ReadHalf<UnixStream>>,
mut reader: BufReader<tokio::io::ReadHalf<IpcStream>>,
worker_id: String,
workers: Arc<RwLock<HashMap<String, Arc<Mutex<ConnectedWorker>>>>>,
tx: mpsc::Sender<(String, WorkerMessage)>,
Expand Down Expand Up @@ -269,10 +255,7 @@ impl IpcServer {

impl Drop for IpcServer {
fn drop(&mut self) {
// Clean up socket file
if self.socket_path.exists() {
let _ = std::fs::remove_file(&self.socket_path);
}
let _ = transport::cleanup(&self.socket_path);
}
}

Expand All @@ -287,12 +270,15 @@ mod tests {
let socket_path = dir.path().join("test.sock");

let mut server = IpcServer::new(&socket_path);
#[cfg(not(windows))]
assert!(!socket_path.exists());

server.start().await.unwrap();
#[cfg(not(windows))]
assert!(socket_path.exists());

server.stop().await.unwrap();
#[cfg(not(windows))]
assert!(!socket_path.exists());
}

Expand Down
Loading