The nixpkgs bump to clippy 0.1.95 / cargo 1.95.0 added + strengthened a large batch of lints. CI denied ALL warnings (`-D warnings`) against the `pedantic = warn` workspace lint, so the bump hard-failed `nix flake check` workspace-wide with zero code changes — and would recur on every future clippy bump. Posture fix (the durable part): CI now runs `-D warnings -A clippy::pedantic`, so the default/correctness/style lints stay a hard gate while the "extra, opinionated" pedantic group is advisory only (still `warn` for local `cargo clippy` via the workspace lints table, just non-blocking in CI). `-A` rather than `-W` so the group drop doesn't re-enable the specific pedantic lints the workspace allows (e.g. `must_use_candidate`). Also fixes the genuine DEFAULT/STYLE lints the bump surfaced across the workspace (doc_lazy_continuation, collapsible_if, ptr_arg, match_like_matches_macro, …) via `cargo clippy --fix` + manual stragglers (`too_many_arguments` #[allow] on the host-config constructors), and three tests that had rotted while the CI runner was offline (#1221): - topology::top_level_agents_in_multi_root — hardcoded unsorted expected - rebuild_queue::depends_on_evicted_dep_counts_as_resolved — needs MAX_HISTORY_PER_KIND newer terminals to evict, not one - coordinator::agent_paths doctest — illustrative pseudo-code, now `ignore` Validated: clippy + formatting + cargo-test checks all pass.
106 lines
3.9 KiB
Rust
106 lines
3.9 KiB
Rust
//! Unix socket server: the daemon listens here, the stdio MCP bridge
|
|
//! `connect()`s on every tool call. One JSON request line in, one
|
|
//! JSON response line out. Connections are short-lived (per tool call)
|
|
//! so the loop is just accept → dispatch → reply → close.
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::{Context, Result};
|
|
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader};
|
|
use tokio::net::{UnixListener, UnixStream};
|
|
|
|
use crate::protocol::{DaemonRequest, DaemonResponse, TaskStatus};
|
|
use crate::runner;
|
|
|
|
/// Start listening on `socket_path` and serve forever. Removes any
|
|
/// stale socket file first so a daemon restart doesn't hit EADDRINUSE.
|
|
pub async fn serve(socket_path: &Path) -> Result<()> {
|
|
let _ = tokio::fs::remove_file(socket_path).await;
|
|
if let Some(parent) = socket_path.parent() {
|
|
tokio::fs::create_dir_all(parent)
|
|
.await
|
|
.with_context(|| format!("mkdir {}", parent.display()))?;
|
|
}
|
|
let listener = UnixListener::bind(socket_path)
|
|
.with_context(|| format!("bind unix socket {}", socket_path.display()))?;
|
|
tracing::info!(path = %socket_path.display(), "bash daemon socket up");
|
|
loop {
|
|
let (stream, _) = listener
|
|
.accept()
|
|
.await
|
|
.context("accept on bash daemon socket")?;
|
|
tokio::spawn(async move {
|
|
if let Err(e) = handle_connection(stream).await {
|
|
tracing::warn!(error = %e, "bash socket connection error");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async fn handle_connection(stream: UnixStream) -> Result<()> {
|
|
let (reader, mut writer) = stream.into_split();
|
|
let mut lines = BufReader::new(reader).lines();
|
|
while let Some(line) = lines.next_line().await? {
|
|
let response = match serde_json::from_str::<DaemonRequest>(&line) {
|
|
Ok(req) => dispatch(req).await,
|
|
Err(e) => DaemonResponse::error(format!("parse request: {e}")),
|
|
};
|
|
let mut json = serde_json::to_string(&response)?;
|
|
json.push('\n');
|
|
writer.write_all(json.as_bytes()).await?;
|
|
writer.flush().await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn dispatch(req: DaemonRequest) -> DaemonResponse {
|
|
match req {
|
|
DaemonRequest::Ping => DaemonResponse::ok(&serde_json::json!({"ok": true})),
|
|
|
|
DaemonRequest::BashRun {
|
|
cmd,
|
|
timeout_secs,
|
|
wait_seconds,
|
|
} => {
|
|
let id = match runner::submit_task(cmd, timeout_secs) {
|
|
Ok(id) => id,
|
|
Err(e) => return DaemonResponse::error(format!("submit_task: {e:#}")),
|
|
};
|
|
// Inline wait: if requested and the task finishes quickly,
|
|
// return the full status instead of just the task ID.
|
|
let wait = wait_seconds.unwrap_or(0);
|
|
if wait > 0
|
|
&& let Some(task) = runner::wait_for_task(&id, wait).await
|
|
&& matches!(
|
|
task.status,
|
|
TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted
|
|
)
|
|
{
|
|
return DaemonResponse::ok(&serde_json::json!({
|
|
"id": id,
|
|
"finished": true,
|
|
"task": task,
|
|
}));
|
|
}
|
|
DaemonResponse::ok(&serde_json::json!({ "id": id, "finished": false }))
|
|
}
|
|
|
|
DaemonRequest::BashStatus { id, wait_seconds } => {
|
|
let wait = wait_seconds.unwrap_or(0);
|
|
let task = if wait > 0 {
|
|
runner::wait_for_task(&id, wait).await
|
|
} else {
|
|
runner::read_task(&id)
|
|
};
|
|
match task {
|
|
Some(t) => DaemonResponse::ok(&t),
|
|
None => DaemonResponse::error(format!("unknown task id `{id}`")),
|
|
}
|
|
}
|
|
|
|
DaemonRequest::ActiveTasks => {
|
|
let tasks = runner::active_tasks();
|
|
DaemonResponse::ok(&tasks)
|
|
}
|
|
}
|
|
}
|