Skip to main content

scv_server/
lib.rs

1//! SCV's authoritative stdio server.
2
3pub mod components;
4mod config;
5
6use std::{
7    collections::{HashMap, VecDeque},
8    io::Read as _,
9    path::{Path, PathBuf},
10    sync::{
11        Arc,
12        atomic::{AtomicU64, AtomicUsize, Ordering},
13    },
14    time::Duration,
15};
16
17use anyhow::{Context, Result, anyhow};
18use async_trait::async_trait;
19use config::Config;
20pub use config::{ApprovalPolicy, ConfigOverrides, user_home_path};
21use sha2::{Digest, Sha256};
22pub fn init_user_config() -> anyhow::Result<std::path::PathBuf> {
23    config::Config::init_user_config()
24}
25pub fn update_index_url(workspace: &std::path::Path) -> anyhow::Result<Option<String>> {
26    Ok(config::Config::load(workspace, ConfigOverrides::default())?
27        .update
28        .index_url)
29}
30
31/// Return the user service name for the selected SCV instance.
32pub fn service_name() -> anyhow::Result<String> {
33    if std::env::var_os("SCV_HOME").is_none() {
34        return Ok("scv.service".into());
35    }
36    let home = user_home_path().ok_or_else(|| anyhow!("cannot determine SCV instance home"))?;
37    let digest = Sha256::digest(home.to_string_lossy().as_bytes());
38    let suffix = digest[..8]
39        .iter()
40        .map(|byte| format!("{byte:02x}"))
41        .collect::<String>();
42    Ok(format!("scv-{suffix}.service"))
43}
44
45pub fn service_unit_path() -> anyhow::Result<std::path::PathBuf> {
46    let config =
47        dirs::config_dir().ok_or_else(|| anyhow!("cannot determine XDG config directory"))?;
48    Ok(config.join("systemd/user").join(service_name()?))
49}
50use scv_core::{
51    AgentError, AgentRuntime, ApprovalGate, ApprovalRequest, BudgetContextPolicy, CoreEvent,
52    EventSink, Message, ToolRegistry, ToolRisk,
53};
54use scv_protocol::{ClientMessage, PROTOCOL_VERSION, PeerInfo, QueueEntry, ServerEvent, Usage};
55use scv_provider_openai::OpenAiProvider;
56use scv_tools::{SkillMap, builtin_registry};
57use tokio::{
58    io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt, BufReader},
59    net::{UnixListener, UnixStream},
60    sync::{Mutex, OwnedSemaphorePermit, Semaphore, mpsc, oneshot},
61    task::JoinHandle,
62};
63use tokio_util::sync::CancellationToken;
64use tokio_util::task::TaskTracker;
65use uuid::Uuid;
66
67const PROMPT_LIMIT_BYTES: usize = 256 * 1024;
68const OUTPUT_QUEUE_CAPACITY: usize = 256;
69const OUTPUT_QUEUE_MIN_BYTES: usize = 16 * 1024 * 1024;
70const SHUTDOWN_GRACE: Duration = Duration::from_secs(3);
71const MAX_QUEUE_ITEMS: usize = 64;
72const MAX_QUEUE_BYTES: usize = 4 * 1024 * 1024;
73
74pub async fn run_stdio(overrides: ConfigOverrides) -> Result<()> {
75    let stdin = tokio::io::stdin();
76    let stdout = tokio::io::stdout();
77    let tasks = TaskTracker::new();
78    let result = run_managed(
79        stdin,
80        stdout,
81        overrides,
82        None,
83        CancellationToken::new(),
84        tasks.clone(),
85    )
86    .await;
87    tasks.close();
88    tasks.wait().await;
89    result
90}
91
92/// Return the local Unix socket used by the SCV daemon and TUI.
93pub fn default_socket_path() -> Result<PathBuf> {
94    scv_client::default_socket_path()
95}
96
97/// Run the authoritative server on the local Unix socket.
98pub async fn run_socket(path: &Path, overrides: ConfigOverrides) -> Result<()> {
99    if let Some(parent) = path.parent() {
100        tokio::fs::create_dir_all(parent)
101            .await
102            .context("create SCV socket directory")?;
103    }
104    let _lock = SocketLock::acquire(path)?;
105    if path.exists() {
106        if UnixStream::connect(path).await.is_ok() {
107            return Err(anyhow!(
108                "SCV server is already running at {}",
109                path.display()
110            ));
111        }
112        use std::os::unix::fs::FileTypeExt;
113        if !std::fs::symlink_metadata(path)?.file_type().is_socket() {
114            return Err(anyhow!(
115                "refusing to remove a non-socket at SCV socket path"
116            ));
117        }
118        tokio::fs::remove_file(path)
119            .await
120            .with_context(|| format!("remove stale SCV socket {}", path.display()))?;
121    }
122    let listener = UnixListener::bind(path)
123        .with_context(|| format!("bind SCV server socket {}", path.display()))?;
124    #[cfg(unix)]
125    {
126        use std::os::unix::fs::PermissionsExt;
127        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
128            .context("secure SCV socket")?;
129    }
130    let components = Arc::new(Mutex::new(components::Components::new(
131        path.to_owned(),
132        std::env::current_dir()?,
133    )));
134    let cancellation = CancellationToken::new();
135    let tasks = TaskTracker::new();
136    let mut clients = tokio::task::JoinSet::new();
137    let refresh_components = components.clone();
138    let refresh_cancel = cancellation.clone();
139    let mut refresh_task = tokio::spawn(async move {
140        let mut refresh = tokio::time::interval(Duration::from_secs(2));
141        loop {
142            tokio::select! {
143                biased;
144                _ = refresh_cancel.cancelled() => break,
145                _ = refresh.tick() => {
146                    tokio::select! {
147                        biased;
148                        _ = refresh_cancel.cancelled() => break,
149                        result = async { refresh_components.lock().await.reconcile().await } => {
150                            if result.is_err() { tracing::warn!("Component account discovery failed"); }
151                        }
152                    }
153                }
154            }
155        }
156    });
157    let _refresh_abort = AbortGuard(refresh_task.abort_handle());
158    let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
159    let result = loop {
160        tokio::select! {
161            accepted = listener.accept() => {
162                let (stream, _) = match accepted { Ok(value) => value, Err(error) => break Err(error.into()) };
163                let child_overrides = overrides.clone();
164                let components = components.clone();
165                let cancellation = cancellation.clone();
166                let tasks = tasks.clone();
167                clients.spawn(async move {
168                    let (reader, writer) = stream.into_split();
169                    if run_managed(reader, writer, child_overrides, Some(components), cancellation, tasks).await.is_err() {
170                        tracing::warn!("SCV socket client stopped");
171                    }
172                });
173            }
174            _ = clients.join_next(), if !clients.is_empty() => {},
175            _ = tokio::signal::ctrl_c() => break Ok(()),
176            _ = terminate.recv() => break Ok(()),
177        }
178    };
179    drop(listener);
180    cancellation.cancel();
181    let _ = (&mut refresh_task).await;
182    components.lock().await.shutdown().await;
183    if tokio::time::timeout(Duration::from_secs(8), async {
184        while clients.join_next().await.is_some() {}
185    })
186    .await
187    .is_err()
188    {
189        clients.abort_all();
190        while clients.join_next().await.is_some() {}
191    }
192    tasks.close();
193    tasks.wait().await;
194    let _ = tokio::fs::remove_file(path).await;
195    result
196}
197
198async fn run_managed<R, W>(
199    reader: R,
200    writer: W,
201    overrides: ConfigOverrides,
202    components: Option<Arc<Mutex<components::Components>>>,
203    cancellation: CancellationToken,
204    tasks: TaskTracker,
205) -> Result<()>
206where
207    R: tokio::io::AsyncRead + Unpin,
208    W: tokio::io::AsyncWrite + Unpin + Send + 'static,
209{
210    let initial_output_bytes =
211        output_queue_bytes(Config::default().protocol.max_server_frame_bytes)?;
212    let (output_tx, mut output_rx) = outbound_channel(initial_output_bytes);
213    let mut writer_task = tasks.spawn(async move {
214        let mut writer = writer;
215        while let Some(frame) = output_rx.recv().await {
216            writer.write_all(&frame.bytes).await?;
217            writer.write_all(b"\n").await?;
218            writer.flush().await?;
219        }
220        Ok::<(), std::io::Error>(())
221    });
222    let _writer_abort = AbortGuard(writer_task.abort_handle());
223    let (done_tx, mut done_rx) = mpsc::channel::<TurnDone>(4);
224    let approvals = Arc::new(ApprovalBroker::default());
225    let mut reader = BufReader::new(reader);
226    let mut frames = FrameBuffer::default();
227    let mut initialized = false;
228    let mut session: Option<Session> = None;
229    let mut active: Option<ActiveTurn> = None;
230    let mut fatal = false;
231    let mut writer_finished = false;
232
233    let loop_result: Result<()> = async {
234        loop {
235        let frame_limit = session.as_ref().map_or_else(
236            || Config::default().protocol.max_client_frame_bytes,
237            |value| value.config.protocol.max_client_frame_bytes,
238        );
239        tokio::select! {
240            _ = cancellation.cancelled() => break,
241            read = frames.read(&mut reader, frame_limit) => {
242                let frame = match read.context("read protocol input")? {
243                    FrameRead::Eof => {
244                        if let Some(active) = &active { active.cancellation.cancel(); }
245                        break;
246                    }
247                    FrameRead::TooLarge => {
248                        send_error(&output_tx, "", "invalid_request", "client frame exceeds configured limit", false, server_frame_limit(&session)).await?;
249                        continue;
250                    }
251                    FrameRead::Frame(frame) => frame,
252                };
253                if frame.is_empty() {
254                    send_error(&output_tx, "", "invalid_json", "protocol frame is empty", false, server_frame_limit(&session)).await?;
255                    continue;
256                }
257                let message = match serde_json::from_slice::<ClientMessage>(&frame) {
258                    Ok(message) => message,
259                    Err(error) => {
260                        send_error(&output_tx, "", "invalid_json", &format!("invalid protocol JSON: {error}"), false, server_frame_limit(&session)).await?;
261                        continue;
262                    }
263                };
264                match message {
265                    ClientMessage::Initialize { request_id, protocol_version, .. } => {
266                        if initialized {
267                            send_error(&output_tx, &request_id, "invalid_request", "connection is already initialized", false, server_frame_limit(&session)).await?;
268                            continue;
269                        }
270                        if protocol_version != PROTOCOL_VERSION {
271                            send_error(&output_tx, &request_id, "version_mismatch", &format!("server supports protocol {PROTOCOL_VERSION}"), true, server_frame_limit(&session)).await?;
272                            fatal = true;
273                            break;
274                        }
275                        initialized = true;
276                        send_event(&output_tx, ServerEvent::Initialized {
277                            request_id,
278                            protocol_version: PROTOCOL_VERSION,
279                            server: PeerInfo { name: "scv-server".into(), version: env!("CARGO_PKG_VERSION").into() },
280                        }, Config::default().protocol.max_server_frame_bytes).await?;
281                    }
282                    other if !initialized => {
283                        send_error(&output_tx, other.request_id(), "not_initialized", "initialize must be the first message", false, server_frame_limit(&session)).await?;
284                    }
285                    ClientMessage::DaemonControl { request_id, command } => {
286                        if let Some(components) = &components {
287                            let result = tokio::select! {
288                                biased;
289                                _ = cancellation.cancelled() => break,
290                                result = async { components.lock().await.control(command).await } => result,
291                            };
292                            match result {
293                                Ok(status) => send_event(&output_tx, ServerEvent::DaemonStatus { request_id, status }, server_frame_limit(&session)).await?,
294                                Err(_) => send_error(&output_tx, &request_id, "component_error", "Component operation failed; check account credentials, private file permissions and absolute workspace", false, server_frame_limit(&session)).await?,
295                            }
296                        } else {
297                            send_error(&output_tx, &request_id, "unsupported", "Component management requires the daemon socket", false, server_frame_limit(&session)).await?;
298                        }
299                    }
300                    ClientMessage::SessionStart { request_id, cwd, provider, model, base_url, no_tools } => {
301                        if session.is_some() {
302                            send_error(&output_tx, &request_id, "invalid_request", "this connection already has a session", false, server_frame_limit(&session)).await?;
303                            continue;
304                        }
305                        let session_overrides = ConfigOverrides {
306                            provider: provider.or_else(|| overrides.provider.clone()),
307                            model: model.or_else(|| overrides.model.clone()),
308                            base_url: base_url.or_else(|| overrides.base_url.clone()),
309                            approval_policy: overrides.approval_policy,
310                            no_tools: no_tools.unwrap_or(overrides.no_tools),
311                        };
312                        match build_session(&cwd, session_overrides).await {
313                            Ok(new_session) => {
314                                output_tx.ensure_capacity(output_queue_bytes(
315                                    new_session.config.protocol.max_server_frame_bytes,
316                                )?)?;
317                                let event = ServerEvent::SessionStarted {
318                                    request_id,
319                                    session_id: new_session.id.clone(),
320                                    cwd: new_session.workspace.display().to_string(),
321                                    model: new_session.runtime.model().to_owned(),
322                                    context_max_tokens: new_session.config.context.max_tokens,
323                                    max_server_frame_bytes: new_session.config.protocol.max_server_frame_bytes,
324                                    max_transcript_bytes: new_session.config.tui.max_transcript_bytes,
325                                    max_transcript_items: new_session.config.tui.max_transcript_items,
326                                    max_prompt_history_bytes: new_session.config.tui.max_prompt_history_bytes,
327                                    max_prompt_history_items: new_session.config.tui.max_prompt_history_items,
328                                };
329                                send_event(&output_tx, event, new_session.config.protocol.max_server_frame_bytes).await?;
330                                send_event(&output_tx, ServerEvent::QueueSnapshot {
331                                    request_id: None,
332                                    session_id: new_session.id.clone(),
333                                    seq: next_seq(&new_session.seq),
334                                    entries: new_session.queue.lock().await.iter().cloned().collect(),
335                                    paused: new_session.paused.load(Ordering::Acquire),
336                                }, new_session.config.protocol.max_server_frame_bytes).await?;
337                                session = Some(new_session);
338                            }
339                            Err(error) => {
340                                send_error(&output_tx, &request_id, "invalid_request", &error.to_string(), false, server_frame_limit(&session)).await?;
341                            }
342                        }
343                    }
344                    ClientMessage::SessionAttach { request_id, .. } => {
345                        send_error(&output_tx, &request_id, "unsupported", "session attach requires the shared socket server", false, server_frame_limit(&session)).await?;
346                    }
347                    ClientMessage::TurnStart { request_id, session_id, prompt } => {
348                        let Some(current) = session.as_ref() else {
349                            send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?;
350                            continue;
351                        };
352                        if current.id != session_id {
353                            send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?;
354                            continue;
355                        }
356                        if prompt.trim().is_empty() || prompt.len() > PROMPT_LIMIT_BYTES {
357                            send_error(&output_tx, &request_id, "invalid_request", "prompt must be non-empty and no larger than 256 KiB", false, server_frame_limit(&session)).await?;
358                            continue;
359                        }
360                        if active.is_some() {
361                            let entry = match current.enqueue(prompt, request_id.clone()).await {
362                                Ok(entry) => entry,
363                                Err(code) => { send_error(&output_tx, &request_id, code, "session queue limit reached", false, server_frame_limit(&session)).await?; continue; }
364                            };
365                            let position = current.queue.lock().await.len().saturating_sub(1);
366                            send_event(&output_tx, ServerEvent::QueueEnqueued {
367                                request_id, session_id: current.id.clone(), seq: next_seq(&current.seq), entry, position,
368                            }, current.config.protocol.max_server_frame_bytes).await?;
369                            continue;
370                        }
371                        let turn_id = Uuid::new_v4().to_string();
372                        let cancellation = cancellation.child_token();
373                        let meta = TurnMeta {
374                            request_id: request_id.clone(),
375                            session_id: current.id.clone(),
376                            turn_id: turn_id.clone(),
377                            seq: Arc::clone(&current.seq),
378                            max_server_frame: current.config.protocol.max_server_frame_bytes,
379                        };
380                        send_event(&output_tx, ServerEvent::TurnStarted {
381                            request_id: request_id.clone(),
382                            session_id: current.id.clone(),
383                            turn_id: turn_id.clone(),
384                            seq: next_seq(&current.seq),
385                        }, current.config.protocol.max_server_frame_bytes).await?;
386                        let runtime = Arc::clone(&current.runtime);
387                        let history = Arc::clone(&current.history);
388                        let sink: Arc<dyn EventSink> = Arc::new(ProtocolSink {
389                            meta: meta.clone(),
390                            output: output_tx.clone(),
391                            cancellation: cancellation.clone(),
392                        });
393                        let gate: Arc<dyn ApprovalGate> = Arc::new(ProtocolApprovalGate {
394                            policy: current.config.tools.approval_policy,
395                            broker: Arc::clone(&approvals),
396                            meta,
397                            output: output_tx.clone(),
398                        });
399                        let task_cancel = cancellation.clone();
400                        let task_done = done_tx.clone();
401                        let task_request = request_id.clone();
402                        let task_session = current.id.clone();
403                        let task_turn = turn_id.clone();
404                        let task = tasks.spawn(async move {
405                            let mut history = history.lock().await;
406                            let result = runtime.run_turn(&mut history, prompt, sink, gate, task_cancel).await;
407                            let _ = task_done.send(TurnDone {
408                                request_id: task_request,
409                                session_id: task_session,
410                                turn_id: task_turn,
411                                result,
412                            }).await;
413                        });
414                        active = Some(ActiveTurn { turn_id, cancellation, task });
415                    }
416                    ClientMessage::QueueUpdate { request_id, session_id, queue_id, revision, prompt } => {
417                        let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
418                        if current.id != session_id { send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?; continue; }
419                        if prompt.trim().is_empty() || prompt.len() > PROMPT_LIMIT_BYTES { send_error(&output_tx, &request_id, "invalid_request", "prompt must be non-empty and no larger than 256 KiB", false, server_frame_limit(&session)).await?; continue; }
420                        match current.update_queue(&queue_id, revision, prompt).await {
421                            Ok(entry) => send_event(&output_tx, ServerEvent::QueueUpdated { request_id, session_id: current.id.clone(), seq: next_seq(&current.seq), entry }, current.config.protocol.max_server_frame_bytes).await?,
422                            Err(code) => send_error(&output_tx, &request_id, code, "queue entry was not found or revision is stale", false, server_frame_limit(&session)).await?,
423                        }
424                    }
425                    ClientMessage::QueueMove { request_id, session_id, queue_id, revision, before_queue_id } => {
426                        let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
427                        match current.move_queue(&session_id, &queue_id, revision, before_queue_id).await {
428                            Ok((id, rev, pos)) => send_event(&output_tx, ServerEvent::QueueMoved { request_id, session_id: current.id.clone(), seq: next_seq(&current.seq), queue_id: id, position: pos, revision: rev }, current.config.protocol.max_server_frame_bytes).await?,
429                            Err(code) => send_error(&output_tx, &request_id, code, "queue entry was not found or revision is stale", false, server_frame_limit(&session)).await?,
430                        }
431                    }
432                    ClientMessage::QueueRemove { request_id, session_id, queue_id, revision } => {
433                        let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
434                        match current.remove_queue(&session_id, &queue_id, revision).await {
435                            Ok((id, rev)) => send_event(&output_tx, ServerEvent::QueueRemoved { request_id, session_id: current.id.clone(), seq: next_seq(&current.seq), queue_id: id, revision: rev }, current.config.protocol.max_server_frame_bytes).await?,
436                            Err(code) => send_error(&output_tx, &request_id, code, "queue entry was not found or revision is stale", false, server_frame_limit(&session)).await?,
437                        }
438                    }
439                    ClientMessage::SessionPause { request_id, session_id, paused } => {
440                        let Some(current) = session.as_ref() else { send_error(&output_tx, &request_id, "session_not_found", "start a session first", false, server_frame_limit(&session)).await?; continue; };
441                        if current.id != session_id { send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?; continue; }
442                        current.paused.store(paused, Ordering::Release);
443                        send_event(&output_tx, ServerEvent::SessionPaused { request_id, session_id: current.id.clone(), seq: next_seq(&current.seq), paused }, current.config.protocol.max_server_frame_bytes).await?;
444                    }
445                    ClientMessage::TurnCancel { request_id, session_id, turn_id } => {
446                        match (&session, &active) {
447                            (Some(current), Some(running)) if current.id == session_id && running.turn_id == turn_id => running.cancellation.cancel(),
448                            _ => send_error(&output_tx, &request_id, "turn_not_found", "active turn was not found", false, server_frame_limit(&session)).await?,
449                        }
450                    }
451                    ClientMessage::ApprovalResolve { request_id, session_id, approval_id, approved } => {
452                        if session.as_ref().is_none_or(|current| current.id != session_id) {
453                            send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?;
454                        } else if !approvals.resolve(&approval_id, approved).await {
455                            send_error(&output_tx, &request_id, "approval_not_found", "approval was not found or already resolved", false, server_frame_limit(&session)).await?;
456                        }
457                    }
458                    ClientMessage::SessionClear { request_id, session_id } => {
459                        let Some(current) = session.as_ref() else {
460                            send_error(&output_tx, &request_id, "session_not_found", "session was not found", false, server_frame_limit(&session)).await?;
461                            continue;
462                        };
463                        if current.id != session_id {
464                            send_error(&output_tx, &request_id, "session_not_found", "session id does not match", false, server_frame_limit(&session)).await?;
465                        } else if active.is_some() {
466                            send_error(&output_tx, &request_id, "turn_active", "cancel the active turn before clearing", false, server_frame_limit(&session)).await?;
467                        } else {
468                            current.history.lock().await.clear();
469                            current.queue.lock().await.clear();
470                            send_event(&output_tx, ServerEvent::SessionCleared {
471                                request_id,
472                                session_id: current.id.clone(),
473                                seq: next_seq(&current.seq),
474                            }, current.config.protocol.max_server_frame_bytes).await?;
475                            send_event(&output_tx, ServerEvent::QueueSnapshot { request_id: None, session_id: current.id.clone(), seq: next_seq(&current.seq), entries: Vec::new(), paused: current.paused.load(Ordering::Acquire) }, current.config.protocol.max_server_frame_bytes).await?;
476                        }
477                    }
478                }
479            }
480            writer = &mut writer_task => {
481                writer_finished = true;
482                writer.context("join protocol writer")??;
483                break;
484            }
485            done = done_rx.recv(), if active.is_some() => {
486                if let Some(done) = done {
487                    if let Some(current) = session.as_ref() {
488                        let seq = next_seq(&current.seq);
489                        let event = match done.result {
490                            Ok(outcome) => ServerEvent::TurnCompleted {
491                                request_id: done.request_id,
492                                session_id: done.session_id,
493                                turn_id: done.turn_id,
494                                seq,
495                                steps: outcome.steps,
496                                usage: Usage { input_tokens: outcome.usage.input_tokens, output_tokens: outcome.usage.output_tokens },
497                            },
498                            Err(AgentError::Cancelled) => ServerEvent::TurnCancelled {
499                                request_id: done.request_id,
500                                session_id: done.session_id,
501                                turn_id: done.turn_id,
502                                seq,
503                            },
504                            Err(error) => ServerEvent::TurnFailed {
505                                request_id: done.request_id,
506                                session_id: done.session_id,
507                                turn_id: done.turn_id,
508                                seq,
509                                code: error.code().into(),
510                                message: error.to_string(),
511                            },
512                        };
513                        send_event(&output_tx, event, current.config.protocol.max_server_frame_bytes).await?;
514                    }
515                    if let Some(mut active) = active.take() {
516                        let _ = (&mut active.task).await;
517                    }
518                    if let Some(current) = session.as_ref()
519                        && !current.paused.load(Ordering::Acquire)
520                        && let Some(entry) = current.queue.lock().await.pop_front()
521                    {
522                        let turn_id = Uuid::new_v4().to_string();
523                        let cancellation = cancellation.child_token();
524                        send_event(&output_tx, ServerEvent::QueueDequeued {
525                            request_id: entry.submitter.clone(),
526                            session_id: current.id.clone(),
527                            seq: next_seq(&current.seq),
528                            queue_id: entry.queue_id,
529                            turn_id: turn_id.clone(),
530                        }, current.config.protocol.max_server_frame_bytes).await?;
531                        send_event(&output_tx, ServerEvent::TurnStarted {
532                            request_id: entry.submitter.clone(),
533                            session_id: current.id.clone(),
534                            turn_id: turn_id.clone(),
535                            seq: next_seq(&current.seq),
536                        }, current.config.protocol.max_server_frame_bytes).await?;
537                        let meta = TurnMeta {
538                            request_id: entry.submitter.clone(), session_id: current.id.clone(), turn_id: turn_id.clone(),
539                            seq: Arc::clone(&current.seq), max_server_frame: current.config.protocol.max_server_frame_bytes,
540                        };
541                        let sink: Arc<dyn EventSink> = Arc::new(ProtocolSink { meta: meta.clone(), output: output_tx.clone(), cancellation: cancellation.clone() });
542                        let gate: Arc<dyn ApprovalGate> = Arc::new(ProtocolApprovalGate { policy: current.config.tools.approval_policy, broker: Arc::clone(&approvals), meta, output: output_tx.clone() });
543                        let runtime = Arc::clone(&current.runtime);
544                        let history = Arc::clone(&current.history);
545                        let task_done = done_tx.clone();
546                        let task_request = entry.submitter;
547                        let task_session = current.id.clone();
548                        let task_turn = turn_id.clone();
549                        let task_cancel = cancellation.clone();
550                        let task = tasks.spawn(async move {
551                            let mut history = history.lock().await;
552                            let result = runtime.run_turn(&mut history, entry.prompt, sink, gate, task_cancel).await;
553                            let _ = task_done.send(TurnDone { request_id: task_request, session_id: task_session, turn_id: task_turn, result }).await;
554                        });
555                        active = Some(ActiveTurn { turn_id, cancellation, task });
556                    }
557                }
558            }
559        }
560        }
561        Ok(())
562    }
563    .await;
564
565    if let Some(active) = active.take() {
566        shutdown_active_turn(active, SHUTDOWN_GRACE).await;
567    }
568    drop(output_tx);
569    let writer_result = if writer_finished {
570        Ok(())
571    } else {
572        shutdown_writer(writer_task, SHUTDOWN_GRACE).await
573    };
574    loop_result?;
575    writer_result?;
576    if fatal {
577        return Err(anyhow!("protocol version mismatch"));
578    }
579    Ok(())
580}
581
582enum FrameRead {
583    Eof,
584    Frame(Vec<u8>),
585    TooLarge,
586}
587
588struct OutboundFrame {
589    bytes: Vec<u8>,
590    _byte_permit: OwnedSemaphorePermit,
591}
592
593#[derive(Clone)]
594struct OutboundSender {
595    frames: mpsc::Sender<OutboundFrame>,
596    budget: Arc<Semaphore>,
597    capacity: Arc<AtomicUsize>,
598}
599
600#[derive(Debug, PartialEq, Eq)]
601enum OutboundSendError {
602    Cancelled,
603    Closed,
604    TimedOut,
605    FrameExceedsQueue { frame_bytes: usize, capacity: usize },
606}
607
608impl std::fmt::Display for OutboundSendError {
609    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610        match self {
611            Self::Cancelled => formatter.write_str("outbound send cancelled"),
612            Self::Closed => formatter.write_str("protocol client disconnected"),
613            Self::TimedOut => formatter.write_str("outbound send timed out under backpressure"),
614            Self::FrameExceedsQueue {
615                frame_bytes,
616                capacity,
617            } => write!(
618                formatter,
619                "outbound frame uses {frame_bytes} bytes but queue capacity is {capacity} bytes"
620            ),
621        }
622    }
623}
624
625impl std::error::Error for OutboundSendError {}
626
627fn outbound_channel(capacity: usize) -> (OutboundSender, mpsc::Receiver<OutboundFrame>) {
628    let (frames, receiver) = mpsc::channel(OUTPUT_QUEUE_CAPACITY);
629    (
630        OutboundSender {
631            frames,
632            budget: Arc::new(Semaphore::new(capacity)),
633            capacity: Arc::new(AtomicUsize::new(capacity)),
634        },
635        receiver,
636    )
637}
638
639impl OutboundSender {
640    fn ensure_capacity(&self, required: usize) -> Result<()> {
641        if required > Semaphore::MAX_PERMITS {
642            return Err(anyhow!(
643                "outbound queue capacity {required} exceeds runtime limit {}",
644                Semaphore::MAX_PERMITS
645            ));
646        }
647        let current = self.capacity.load(Ordering::Acquire);
648        if required > current {
649            self.budget.add_permits(required - current);
650            self.capacity.store(required, Ordering::Release);
651        }
652        Ok(())
653    }
654
655    async fn send(
656        &self,
657        bytes: Vec<u8>,
658        cancellation: Option<&CancellationToken>,
659    ) -> std::result::Result<(), OutboundSendError> {
660        self.send_with_timeout(bytes, cancellation, SHUTDOWN_GRACE)
661            .await
662    }
663
664    async fn send_with_timeout(
665        &self,
666        bytes: Vec<u8>,
667        cancellation: Option<&CancellationToken>,
668        control_timeout: Duration,
669    ) -> std::result::Result<(), OutboundSendError> {
670        let frame_bytes =
671            bytes
672                .len()
673                .checked_add(1)
674                .ok_or(OutboundSendError::FrameExceedsQueue {
675                    frame_bytes: usize::MAX,
676                    capacity: self.capacity.load(Ordering::Acquire),
677                })?;
678        let capacity = self.capacity.load(Ordering::Acquire);
679        let permits =
680            u32::try_from(frame_bytes).map_err(|_| OutboundSendError::FrameExceedsQueue {
681                frame_bytes,
682                capacity,
683            })?;
684        if frame_bytes > capacity {
685            return Err(OutboundSendError::FrameExceedsQueue {
686                frame_bytes,
687                capacity,
688            });
689        }
690
691        let control_deadline = tokio::time::Instant::now() + control_timeout;
692        let acquire = Arc::clone(&self.budget).acquire_many_owned(permits);
693        let permit = if let Some(cancellation) = cancellation {
694            tokio::select! {
695                biased;
696                _ = cancellation.cancelled() => return Err(OutboundSendError::Cancelled),
697                permit = acquire => permit.map_err(|_| OutboundSendError::Closed)?,
698            }
699        } else {
700            tokio::time::timeout_at(control_deadline, acquire)
701                .await
702                .map_err(|_| OutboundSendError::TimedOut)?
703                .map_err(|_| OutboundSendError::Closed)?
704        };
705        let frame = OutboundFrame {
706            bytes,
707            _byte_permit: permit,
708        };
709        if let Some(cancellation) = cancellation {
710            tokio::select! {
711                biased;
712                _ = cancellation.cancelled() => Err(OutboundSendError::Cancelled),
713                result = self.frames.send(frame) => result.map_err(|_| OutboundSendError::Closed),
714            }
715        } else {
716            tokio::time::timeout_at(control_deadline, self.frames.send(frame))
717                .await
718                .map_err(|_| OutboundSendError::TimedOut)?
719                .map_err(|_| OutboundSendError::Closed)
720        }
721    }
722}
723
724fn output_queue_bytes(max_frame_bytes: usize) -> Result<usize> {
725    let required = max_frame_bytes
726        .checked_add(1)
727        .and_then(|bytes| bytes.checked_mul(2))
728        .ok_or_else(|| anyhow!("configured server frame limit is too large"))?
729        .max(OUTPUT_QUEUE_MIN_BYTES);
730    if required > Semaphore::MAX_PERMITS {
731        return Err(anyhow!(
732            "configured server frame limit requires an outbound queue larger than the runtime supports"
733        ));
734    }
735    Ok(required)
736}
737
738/// A persistent decoder keeps consumed partial bytes across select cancellation.
739#[derive(Default)]
740struct FrameBuffer {
741    bytes: Vec<u8>,
742    oversized: bool,
743}
744
745impl FrameBuffer {
746    async fn read<R>(&mut self, reader: &mut R, max_bytes: usize) -> std::io::Result<FrameRead>
747    where
748        R: AsyncBufRead + Unpin,
749    {
750        loop {
751            let available = reader.fill_buf().await?;
752            let eof = available.is_empty();
753            let end = available.iter().position(|b| *b == b'\n');
754            let take = end.map_or(available.len(), |n| n + 1);
755            if !self.oversized {
756                if self.bytes.len().saturating_add(take) > max_bytes.saturating_add(2) {
757                    self.oversized = true;
758                    self.bytes.clear();
759                } else {
760                    self.bytes.extend_from_slice(&available[..take]);
761                }
762            }
763            reader.consume(take);
764            if end.is_some() || eof {
765                if std::mem::take(&mut self.oversized) {
766                    return Ok(FrameRead::TooLarge);
767                }
768                if eof && self.bytes.is_empty() {
769                    return Ok(FrameRead::Eof);
770                }
771                let mut bytes = std::mem::take(&mut self.bytes);
772                while matches!(bytes.last(), Some(b'\n' | b'\r')) {
773                    bytes.pop();
774                }
775                return Ok(if bytes.len() > max_bytes {
776                    FrameRead::TooLarge
777                } else {
778                    FrameRead::Frame(bytes)
779                });
780            }
781        }
782    }
783}
784
785#[cfg(test)]
786async fn read_bounded_frame<R: AsyncBufRead + Unpin>(
787    reader: &mut R,
788    max_bytes: usize,
789) -> std::io::Result<FrameRead> {
790    FrameBuffer::default().read(reader, max_bytes).await
791}
792
793/// A persistent advisory lock closes the stale-socket unlink/bind race.
794struct SocketLock(std::fs::File);
795impl SocketLock {
796    fn acquire(socket: &Path) -> Result<Self> {
797        use std::os::unix::{fs::OpenOptionsExt, io::AsRawFd};
798        let file = std::fs::OpenOptions::new()
799            .read(true)
800            .write(true)
801            .create(true)
802            .truncate(false)
803            .mode(0o600)
804            .custom_flags(libc::O_NOFOLLOW)
805            .open(socket.with_extension("lock"))?;
806        // SAFETY: flock operates on this owned, live file descriptor.
807        if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
808            return Err(anyhow!("SCV daemon already owns this socket"));
809        }
810        Ok(Self(file))
811    }
812}
813impl Drop for SocketLock {
814    fn drop(&mut self) {
815        use std::os::unix::io::AsRawFd;
816        // SAFETY: the descriptor remains live until this drop returns.
817        unsafe {
818            libc::flock(self.0.as_raw_fd(), libc::LOCK_UN);
819        }
820    }
821}
822
823fn server_frame_limit(session: &Option<Session>) -> usize {
824    session.as_ref().map_or_else(
825        || Config::default().protocol.max_server_frame_bytes,
826        |value| value.config.protocol.max_server_frame_bytes,
827    )
828}
829
830struct Session {
831    id: String,
832    workspace: PathBuf,
833    config: Config,
834    runtime: Arc<AgentRuntime>,
835    history: Arc<Mutex<Vec<Message>>>,
836    seq: Arc<AtomicU64>,
837    queue: Arc<Mutex<VecDeque<QueueEntry>>>,
838    paused: Arc<std::sync::atomic::AtomicBool>,
839}
840
841impl Session {
842    async fn enqueue(
843        &self,
844        prompt: String,
845        submitter: String,
846    ) -> std::result::Result<QueueEntry, &'static str> {
847        let entry = QueueEntry {
848            queue_id: Uuid::new_v4().to_string(),
849            revision: 1,
850            prompt,
851            submitter,
852        };
853        let mut queue = self.queue.lock().await;
854        let bytes: usize = queue.iter().map(|item| item.prompt.len()).sum();
855        if queue.len() >= MAX_QUEUE_ITEMS
856            || bytes.saturating_add(entry.prompt.len()) > MAX_QUEUE_BYTES
857        {
858            return Err("queue_limit");
859        }
860        queue.push_back(entry.clone());
861        Ok(entry)
862    }
863
864    async fn update_queue(
865        &self,
866        id: &str,
867        revision: u64,
868        prompt: String,
869    ) -> std::result::Result<QueueEntry, &'static str> {
870        let mut queue = self.queue.lock().await;
871        let bytes: usize = queue.iter().map(|item| item.prompt.len()).sum();
872        let entry = queue
873            .iter_mut()
874            .find(|entry| entry.queue_id == id)
875            .ok_or("queue_not_found")?;
876        if entry.revision != revision {
877            return Err("queue_conflict");
878        }
879        if bytes
880            .saturating_sub(entry.prompt.len())
881            .saturating_add(prompt.len())
882            > MAX_QUEUE_BYTES
883        {
884            return Err("queue_limit");
885        }
886        entry.prompt = prompt;
887        entry.revision += 1;
888        Ok(entry.clone())
889    }
890
891    async fn move_queue(
892        &self,
893        session_id: &str,
894        id: &str,
895        revision: u64,
896        before: Option<String>,
897    ) -> std::result::Result<(String, u64, usize), &'static str> {
898        if self.id != session_id {
899            return Err("session_not_found");
900        }
901        let mut queue = self.queue.lock().await;
902        let index = queue
903            .iter()
904            .position(|entry| entry.queue_id == id)
905            .ok_or("queue_not_found")?;
906        if queue[index].revision != revision {
907            return Err("queue_conflict");
908        }
909        // Validate the destination while the source is still present. This keeps
910        // the operation atomic and handles a self move as a no-op reorder.
911        let target_index = match before.as_deref() {
912            Some(target) if target == id => return Ok((id.to_string(), revision, index)),
913            Some(target) => Some(
914                queue
915                    .iter()
916                    .position(|item| item.queue_id == target)
917                    .ok_or("queue_not_found")?,
918            ),
919            None => None,
920        };
921        let mut entry = queue.remove(index).expect("queue index exists");
922        let target = target_index.map_or(queue.len(), |target| {
923            target.saturating_sub(usize::from(target > index))
924        });
925        let pos = target.min(queue.len());
926        let id = entry.queue_id.clone();
927        let rev = entry.revision + 1;
928        entry.revision = rev;
929        queue.insert(pos, entry);
930        Ok((id, rev, pos))
931    }
932
933    async fn remove_queue(
934        &self,
935        session_id: &str,
936        id: &str,
937        revision: u64,
938    ) -> std::result::Result<(String, u64), &'static str> {
939        if self.id != session_id {
940            return Err("session_not_found");
941        }
942        let mut queue = self.queue.lock().await;
943        let index = queue
944            .iter()
945            .position(|entry| entry.queue_id == id)
946            .ok_or("queue_not_found")?;
947        if queue[index].revision != revision {
948            return Err("queue_conflict");
949        }
950        let entry = queue.remove(index).expect("queue index exists");
951        Ok((entry.queue_id, entry.revision))
952    }
953}
954
955struct ActiveTurn {
956    turn_id: String,
957    cancellation: CancellationToken,
958    task: JoinHandle<()>,
959}
960
961impl Drop for ActiveTurn {
962    fn drop(&mut self) {
963        self.cancellation.cancel();
964        self.task.abort();
965    }
966}
967
968struct AbortGuard(tokio::task::AbortHandle);
969impl Drop for AbortGuard {
970    fn drop(&mut self) {
971        self.0.abort();
972    }
973}
974
975async fn shutdown_active_turn(mut active: ActiveTurn, grace: Duration) -> bool {
976    active.cancellation.cancel();
977    if tokio::time::timeout(grace, &mut active.task).await.is_ok() {
978        true
979    } else {
980        active.task.abort();
981        let _ = (&mut active.task).await;
982        false
983    }
984}
985
986async fn shutdown_writer(
987    mut writer: JoinHandle<std::io::Result<()>>,
988    grace: Duration,
989) -> Result<()> {
990    match tokio::time::timeout(grace, &mut writer).await {
991        Ok(result) => {
992            result.context("join protocol writer")??;
993            Ok(())
994        }
995        Err(_) => {
996            writer.abort();
997            let _ = writer.await;
998            Err(anyhow!("protocol writer shutdown timed out"))
999        }
1000    }
1001}
1002
1003struct TurnDone {
1004    request_id: String,
1005    session_id: String,
1006    turn_id: String,
1007    result: Result<scv_core::TurnOutcome, AgentError>,
1008}
1009
1010#[derive(Clone)]
1011struct TurnMeta {
1012    request_id: String,
1013    session_id: String,
1014    turn_id: String,
1015    seq: Arc<AtomicU64>,
1016    max_server_frame: usize,
1017}
1018
1019fn next_seq(sequence: &AtomicU64) -> u64 {
1020    sequence.fetch_add(1, Ordering::Relaxed) + 1
1021}
1022
1023async fn build_session(cwd: &str, overrides: ConfigOverrides) -> Result<Session> {
1024    let workspace = std::fs::canonicalize(cwd).with_context(|| format!("resolve cwd {cwd}"))?;
1025    if !workspace.is_dir() {
1026        return Err(anyhow!("cwd is not a directory"));
1027    }
1028    let no_tools = overrides.no_tools;
1029    let config = Config::load(&workspace, overrides)?;
1030    if !no_tools {
1031        config.prepare_adapter_homes()?;
1032    }
1033    let provider_config = config.provider.clone();
1034    let api_key = provider_config.api_key.clone().or_else(|| {
1035        provider_config.api_key_env.as_deref().and_then(|name| std::env::var(name).ok())
1036    }).filter(|key| !key.trim().is_empty()).ok_or_else(|| anyhow!("provider credential is not configured; set provider.api_key or provider.api_key_env"))?;
1037    let (skills, skill_roots, skill_prompt) = discover_skills(&workspace, &config)?;
1038    let system_prompt = build_system_prompt(&workspace, &config, &skill_prompt)?;
1039    let provider = Arc::new(OpenAiProvider::new(
1040        provider_config.model.clone(),
1041        provider_config.base_url.clone(),
1042        api_key,
1043        Duration::from_secs(provider_config.timeout_seconds),
1044        config.provider_limits(),
1045        provider_config.headers.clone(),
1046    )?);
1047    let tools = if no_tools {
1048        Arc::new(ToolRegistry::default())
1049    } else {
1050        Arc::new(builtin_registry(
1051            config.tools(),
1052            skills,
1053            skill_roots,
1054            config.skills.max_skill_bytes,
1055            config.adapters(),
1056        )?)
1057    };
1058    let context = Arc::new(BudgetContextPolicy::new((&config.context).into())?);
1059    let runtime = Arc::new(AgentRuntime::new(
1060        provider,
1061        tools,
1062        context,
1063        config.core_agent(system_prompt),
1064        workspace.clone(),
1065    ));
1066    Ok(Session {
1067        id: Uuid::new_v4().to_string(),
1068        workspace,
1069        config,
1070        runtime,
1071        history: Arc::new(Mutex::new(Vec::new())),
1072        seq: Arc::new(AtomicU64::new(0)),
1073        queue: Arc::new(Mutex::new(VecDeque::new())),
1074        paused: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1075    })
1076}
1077
1078fn build_system_prompt(workspace: &Path, config: &Config, skills: &str) -> Result<String> {
1079    let mut prompt = config.agent.system_prompt.clone();
1080    prompt.push_str(&format!(
1081        "\nCurrent working directory: {}\n",
1082        workspace.display()
1083    ));
1084    let agents_path = workspace.join("AGENTS.md");
1085    if agents_path.is_file() {
1086        let canonical = std::fs::canonicalize(&agents_path).context("resolve project AGENTS.md")?;
1087        if !canonical.starts_with(workspace) {
1088            return Err(anyhow!("project AGENTS.md escaped workspace"));
1089        }
1090        let (bytes, truncated) = read_prefix(&canonical, config.tools.max_read_bytes)
1091            .context("read project AGENTS.md")?;
1092        let instructions = std::str::from_utf8(&bytes).context("project AGENTS.md is not UTF-8")?;
1093        prompt.push_str("\n# Project instructions\n");
1094        prompt.push_str(instructions);
1095        if truncated {
1096            prompt.push_str("\n[AGENTS.md truncated by configured read limit]\n");
1097        }
1098    }
1099    if !skills.is_empty() {
1100        prompt.push_str("\n# Available skills\n");
1101        prompt.push_str(skills);
1102        prompt.push_str("\nUse read_skill with a skill name when its workflow applies.\n");
1103    }
1104    Ok(prompt)
1105}
1106
1107fn discover_skills(workspace: &Path, config: &Config) -> Result<(SkillMap, Vec<PathBuf>, String)> {
1108    let mut skills = SkillMap::new();
1109    let mut roots = Vec::new();
1110    let project_root = workspace.join(&config.skills.project_dir);
1111    for (root, must_be_workspace) in [(&project_root, true), (&config.skills.user_dir, false)] {
1112        if !root.is_dir() {
1113            continue;
1114        }
1115        let canonical = std::fs::canonicalize(root)
1116            .with_context(|| format!("resolve skill root {}", root.display()))?;
1117        if must_be_workspace && !canonical.starts_with(workspace) {
1118            return Err(anyhow!("project skill root escaped workspace"));
1119        }
1120        roots.push(canonical.clone());
1121        let mut entries: Vec<_> = std::fs::read_dir(&canonical)
1122            .with_context(|| format!("read skill root {}", canonical.display()))?
1123            .filter_map(Result::ok)
1124            .collect();
1125        entries.sort_by_key(|entry| entry.file_name());
1126        for entry in entries {
1127            if skills.len() >= config.skills.max_skills {
1128                break;
1129            }
1130            let path = entry.path().join("SKILL.md");
1131            if !path.is_file() {
1132                continue;
1133            }
1134            let canonical_file = std::fs::canonicalize(&path)
1135                .with_context(|| format!("resolve skill {}", path.display()))?;
1136            if !canonical_file.starts_with(&canonical) {
1137                continue;
1138            }
1139            let name = entry.file_name().to_string_lossy().to_string();
1140            skills.entry(name).or_insert(canonical_file);
1141        }
1142    }
1143    let mut names: Vec<_> = skills.keys().cloned().collect();
1144    names.sort();
1145    let mut listing = String::new();
1146    for name in names {
1147        let path = &skills[&name];
1148        let bytes = read_prefix(path, config.skills.max_skill_bytes)
1149            .map(|(bytes, _)| bytes)
1150            .unwrap_or_default();
1151        let content = String::from_utf8_lossy(&bytes);
1152        let description = skill_description(&content);
1153        listing.push_str(&format!("- {name}: {description}\n"));
1154    }
1155    Ok((skills, roots, listing))
1156}
1157
1158fn read_prefix(path: &Path, max_bytes: usize) -> std::io::Result<(Vec<u8>, bool)> {
1159    let file = std::fs::File::open(path)?;
1160    let mut bytes = Vec::with_capacity(max_bytes.min(8192));
1161    file.take(
1162        u64::try_from(max_bytes)
1163            .unwrap_or(u64::MAX)
1164            .saturating_add(1),
1165    )
1166    .read_to_end(&mut bytes)?;
1167    let truncated = bytes.len() > max_bytes;
1168    bytes.truncate(max_bytes);
1169    Ok((bytes, truncated))
1170}
1171
1172fn skill_description(content: &str) -> String {
1173    if let Some(frontmatter) = content.strip_prefix("---\n")
1174        && let Some((header, _)) = frontmatter.split_once("\n---")
1175    {
1176        for line in header.lines() {
1177            if let Some(description) = line.strip_prefix("description:") {
1178                return description.trim().trim_matches('"').to_owned();
1179            }
1180        }
1181    }
1182    content
1183        .lines()
1184        .map(str::trim)
1185        .find(|line| !line.is_empty() && !line.starts_with('#'))
1186        .unwrap_or("No description provided")
1187        .chars()
1188        .take(240)
1189        .collect()
1190}
1191
1192struct ProtocolSink {
1193    meta: TurnMeta,
1194    output: OutboundSender,
1195    cancellation: CancellationToken,
1196}
1197
1198#[async_trait]
1199impl EventSink for ProtocolSink {
1200    async fn emit(&self, event: CoreEvent) -> Result<(), AgentError> {
1201        let seq = next_seq(&self.meta.seq);
1202        let event = match event {
1203            CoreEvent::AssistantDelta { content } => ServerEvent::AssistantDelta {
1204                request_id: self.meta.request_id.clone(),
1205                session_id: self.meta.session_id.clone(),
1206                turn_id: self.meta.turn_id.clone(),
1207                seq,
1208                content,
1209            },
1210            CoreEvent::AssistantCompleted { content } => ServerEvent::AssistantCompleted {
1211                request_id: self.meta.request_id.clone(),
1212                session_id: self.meta.session_id.clone(),
1213                turn_id: self.meta.turn_id.clone(),
1214                seq,
1215                content,
1216            },
1217            CoreEvent::ToolProposed {
1218                call_id,
1219                name,
1220                arguments,
1221            } => ServerEvent::ToolProposed {
1222                request_id: self.meta.request_id.clone(),
1223                session_id: self.meta.session_id.clone(),
1224                turn_id: self.meta.turn_id.clone(),
1225                seq,
1226                call_id,
1227                name,
1228                arguments,
1229            },
1230            CoreEvent::ToolStarted { call_id, name } => ServerEvent::ToolStarted {
1231                request_id: self.meta.request_id.clone(),
1232                session_id: self.meta.session_id.clone(),
1233                turn_id: self.meta.turn_id.clone(),
1234                seq,
1235                call_id,
1236                name,
1237            },
1238            CoreEvent::ToolCompleted {
1239                call_id,
1240                name,
1241                output,
1242            } => ServerEvent::ToolCompleted {
1243                request_id: self.meta.request_id.clone(),
1244                session_id: self.meta.session_id.clone(),
1245                turn_id: self.meta.turn_id.clone(),
1246                seq,
1247                call_id,
1248                name,
1249                success: !output.is_error,
1250                output: output.content,
1251                truncated: output.truncated,
1252            },
1253            CoreEvent::ContextCompacted {
1254                before_tokens,
1255                after_tokens,
1256                removed_messages,
1257            } => ServerEvent::ContextCompacted {
1258                request_id: self.meta.request_id.clone(),
1259                session_id: self.meta.session_id.clone(),
1260                turn_id: self.meta.turn_id.clone(),
1261                seq,
1262                before_tokens,
1263                after_tokens,
1264                removed_messages,
1265            },
1266            CoreEvent::SessionTrimmed {
1267                removed_messages,
1268                history_bytes,
1269            } => ServerEvent::SessionTrimmed {
1270                request_id: self.meta.request_id.clone(),
1271                session_id: self.meta.session_id.clone(),
1272                seq,
1273                removed_messages,
1274                history_bytes,
1275            },
1276        };
1277        send_turn_event(
1278            &self.output,
1279            event,
1280            self.meta.max_server_frame,
1281            &self.cancellation,
1282        )
1283        .await
1284    }
1285}
1286
1287#[derive(Default)]
1288struct ApprovalBroker {
1289    pending: Mutex<HashMap<String, oneshot::Sender<bool>>>,
1290}
1291
1292impl ApprovalBroker {
1293    async fn insert(&self, id: String, sender: oneshot::Sender<bool>) {
1294        self.pending.lock().await.insert(id, sender);
1295    }
1296
1297    async fn remove(&self, id: &str) {
1298        self.pending.lock().await.remove(id);
1299    }
1300
1301    async fn resolve(&self, id: &str, approved: bool) -> bool {
1302        let sender = self.pending.lock().await.remove(id);
1303        sender.is_some_and(|sender| sender.send(approved).is_ok())
1304    }
1305}
1306
1307struct ProtocolApprovalGate {
1308    policy: ApprovalPolicy,
1309    broker: Arc<ApprovalBroker>,
1310    meta: TurnMeta,
1311    output: OutboundSender,
1312}
1313
1314#[async_trait]
1315impl ApprovalGate for ProtocolApprovalGate {
1316    async fn approve(
1317        &self,
1318        request: ApprovalRequest,
1319        cancellation: CancellationToken,
1320    ) -> Result<bool, AgentError> {
1321        match self.policy {
1322            ApprovalPolicy::OnRisk if request.risk == ToolRisk::ReadOnly => return Ok(true),
1323            ApprovalPolicy::Never => return Ok(request.risk == ToolRisk::ReadOnly),
1324            ApprovalPolicy::Always | ApprovalPolicy::OnRisk => {}
1325        }
1326        let approval_id = Uuid::new_v4().to_string();
1327        let (sender, receiver) = oneshot::channel();
1328        self.broker.insert(approval_id.clone(), sender).await;
1329        let event = ServerEvent::ApprovalRequested {
1330            request_id: self.meta.request_id.clone(),
1331            session_id: self.meta.session_id.clone(),
1332            turn_id: self.meta.turn_id.clone(),
1333            seq: next_seq(&self.meta.seq),
1334            approval_id: approval_id.clone(),
1335            call_id: request.call_id,
1336            name: request.name,
1337            risk: request.risk.as_str().into(),
1338            cwd: request.cwd.display().to_string(),
1339            summary: request.summary,
1340        };
1341        if let Err(error) = send_turn_event(
1342            &self.output,
1343            event,
1344            self.meta.max_server_frame,
1345            &cancellation,
1346        )
1347        .await
1348        {
1349            self.broker.remove(&approval_id).await;
1350            return Err(error);
1351        }
1352        tokio::select! {
1353            result = receiver => result.map_err(|_| AgentError::Cancelled),
1354            _ = cancellation.cancelled() => {
1355                self.broker.remove(&approval_id).await;
1356                Err(AgentError::Cancelled)
1357            }
1358        }
1359    }
1360}
1361
1362async fn send_event(output: &OutboundSender, event: ServerEvent, max_bytes: usize) -> Result<()> {
1363    let bytes = encode_event(&event, max_bytes)?;
1364    output.send(bytes, None).await.map_err(anyhow::Error::new)
1365}
1366
1367async fn send_turn_event(
1368    output: &OutboundSender,
1369    event: ServerEvent,
1370    max_bytes: usize,
1371    cancellation: &CancellationToken,
1372) -> Result<(), AgentError> {
1373    let bytes = encode_event(&event, max_bytes)
1374        .map_err(|error| AgentError::ResponseLimit(error.to_string()))?;
1375    match output.send(bytes, Some(cancellation)).await {
1376        Ok(()) => Ok(()),
1377        Err(OutboundSendError::Cancelled) => Err(AgentError::Cancelled),
1378        Err(error) => Err(AgentError::Internal(error.to_string())),
1379    }
1380}
1381
1382fn encode_event(event: &ServerEvent, max_bytes: usize) -> Result<Vec<u8>> {
1383    let bytes = serde_json::to_vec(event).context("serialize protocol event")?;
1384    if bytes.len() > max_bytes {
1385        return Err(anyhow!("server event exceeds configured frame limit"));
1386    }
1387    Ok(bytes)
1388}
1389
1390async fn send_error(
1391    output: &OutboundSender,
1392    request_id: &str,
1393    code: &str,
1394    message: &str,
1395    fatal: bool,
1396    max_bytes: usize,
1397) -> Result<()> {
1398    send_event(
1399        output,
1400        ServerEvent::Error {
1401            request_id: (!request_id.is_empty()).then(|| request_id.to_owned()),
1402            code: code.into(),
1403            message: message.into(),
1404            fatal,
1405        },
1406        max_bytes,
1407    )
1408    .await
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413    use std::{
1414        future::pending,
1415        io::Cursor,
1416        sync::atomic::{AtomicBool, Ordering},
1417    };
1418
1419    use super::*;
1420
1421    struct DropSignal(Arc<AtomicBool>);
1422
1423    impl Drop for DropSignal {
1424        fn drop(&mut self) {
1425            self.0.store(true, Ordering::Release);
1426        }
1427    }
1428
1429    #[tokio::test]
1430    async fn nonreading_management_client_does_not_hold_component_lock() {
1431        let (mut input, server_input) = tokio::io::duplex(65536);
1432        let (server_output, _blocked_output) = tokio::io::duplex(1);
1433        let tasks = TaskTracker::new();
1434        let components = Arc::new(Mutex::new(components::Components::new(
1435            PathBuf::from("/unused.sock"),
1436            PathBuf::from("/"),
1437        )));
1438        let cancel = CancellationToken::new();
1439        let handler = tokio::spawn(run_managed(
1440            server_input,
1441            server_output,
1442            ConfigOverrides::default(),
1443            Some(components.clone()),
1444            cancel.clone(),
1445            tasks.clone(),
1446        ));
1447        input.write_all(b"{\"type\":\"initialize\",\"request_id\":\"init\",\"protocol_version\":2,\"client\":{\"name\":\"test\",\"version\":\"0\"}}\n").await.unwrap();
1448        for _ in 0..300 {
1449            input.write_all(b"{\"type\":\"daemon.control\",\"request_id\":\"s\",\"command\":{\"action\":\"status\"}}\n").await.unwrap();
1450        }
1451        tokio::time::sleep(Duration::from_millis(50)).await;
1452        let status = tokio::time::timeout(Duration::from_millis(100), async {
1453            components.lock().await.status()
1454        })
1455        .await
1456        .unwrap();
1457        assert_eq!(status.pid, std::process::id());
1458        cancel.cancel();
1459        handler.abort();
1460        let _ = handler.await;
1461        tasks.close();
1462        tokio::time::timeout(Duration::from_secs(1), tasks.wait())
1463            .await
1464            .unwrap();
1465    }
1466
1467    #[tokio::test]
1468    async fn forced_connection_abort_drops_and_joins_writer_descendants() {
1469        let (mut input, server_input) = tokio::io::duplex(512);
1470        let (server_output, _blocked_output) = tokio::io::duplex(1);
1471        let tasks = TaskTracker::new();
1472        let handler = tokio::spawn(run_managed(
1473            server_input,
1474            server_output,
1475            ConfigOverrides::default(),
1476            None,
1477            CancellationToken::new(),
1478            tasks.clone(),
1479        ));
1480        input.write_all(b"{\"type\":\"initialize\",\"request_id\":\"init\",\"protocol_version\":2,\"client\":{\"name\":\"test\",\"version\":\"0\"}}\n").await.unwrap();
1481        tokio::time::timeout(Duration::from_secs(1), async {
1482            while tasks.is_empty() {
1483                tokio::task::yield_now().await;
1484            }
1485        })
1486        .await
1487        .unwrap();
1488        handler.abort();
1489        let _ = handler.await;
1490        tasks.close();
1491        tokio::time::timeout(Duration::from_secs(1), tasks.wait())
1492            .await
1493            .unwrap();
1494        assert!(tasks.is_empty());
1495    }
1496
1497    #[tokio::test]
1498    async fn forced_handler_abort_cancels_and_joins_active_turn() {
1499        let tasks = TaskTracker::new();
1500        let cancellation = CancellationToken::new();
1501        let child_cancel = cancellation.child_token();
1502        let observed_cancel = child_cancel.clone();
1503        let dropped = Arc::new(AtomicBool::new(false));
1504        let (ready_tx, ready_rx) = oneshot::channel();
1505        let task = tasks.spawn({
1506            let dropped = dropped.clone();
1507            async move {
1508                let _guard = DropSignal(dropped);
1509                let _ = ready_tx.send(());
1510                pending::<()>().await;
1511            }
1512        });
1513        ready_rx.await.unwrap();
1514        let (owned_tx, owned_rx) = oneshot::channel();
1515        let handler = tokio::spawn(async move {
1516            let _active = ActiveTurn {
1517                turn_id: "test".into(),
1518                cancellation: child_cancel,
1519                task,
1520            };
1521            let _ = owned_tx.send(());
1522            pending::<()>().await;
1523        });
1524        owned_rx.await.unwrap();
1525        handler.abort();
1526        let _ = handler.await;
1527        tasks.close();
1528        tokio::time::timeout(Duration::from_secs(1), tasks.wait())
1529            .await
1530            .unwrap();
1531        assert!(observed_cancel.is_cancelled());
1532        assert!(dropped.load(Ordering::Acquire));
1533    }
1534
1535    #[tokio::test]
1536    async fn frame_buffer_preserves_partial_and_discard_state_across_cancellation() {
1537        let (mut input, output) = tokio::io::duplex(64);
1538        let mut reader = BufReader::new(output);
1539        let mut frames = FrameBuffer::default();
1540        input.write_all(b"12").await.unwrap();
1541        assert!(
1542            tokio::time::timeout(Duration::from_millis(10), frames.read(&mut reader, 4))
1543                .await
1544                .is_err()
1545        );
1546        input.write_all(b"34\n").await.unwrap();
1547        assert!(
1548            matches!(frames.read(&mut reader, 4).await.unwrap(), FrameRead::Frame(value) if value == b"1234")
1549        );
1550        input.write_all(b"123456789").await.unwrap();
1551        assert!(
1552            tokio::time::timeout(Duration::from_millis(10), frames.read(&mut reader, 4))
1553                .await
1554                .is_err()
1555        );
1556        input.write_all(b"\n{}\n").await.unwrap();
1557        assert!(matches!(
1558            frames.read(&mut reader, 4).await.unwrap(),
1559            FrameRead::TooLarge
1560        ));
1561        assert!(
1562            matches!(frames.read(&mut reader, 4).await.unwrap(), FrameRead::Frame(value) if value == b"{}")
1563        );
1564    }
1565
1566    #[tokio::test]
1567    async fn bounded_reader_discards_an_oversized_line() {
1568        let input = format!("{}\n{{}}\n", "x".repeat(10));
1569        let mut reader = BufReader::new(Cursor::new(input.into_bytes()));
1570        assert!(matches!(
1571            read_bounded_frame(&mut reader, 4).await.unwrap(),
1572            FrameRead::TooLarge
1573        ));
1574        match read_bounded_frame(&mut reader, 4).await.unwrap() {
1575            FrameRead::Frame(frame) => assert_eq!(frame, b"{}"),
1576            _ => panic!("expected the frame following the oversized line"),
1577        }
1578    }
1579
1580    #[tokio::test]
1581    async fn bounded_reader_accepts_exact_crlf_limit() {
1582        let mut reader = BufReader::new(Cursor::new(b"1234\r\n".to_vec()));
1583        match read_bounded_frame(&mut reader, 4).await.unwrap() {
1584            FrameRead::Frame(frame) => assert_eq!(frame, b"1234"),
1585            _ => panic!("expected an exact-limit frame"),
1586        }
1587    }
1588
1589    #[tokio::test]
1590    async fn outbound_byte_backpressure_is_cancellation_aware() {
1591        let (output, mut receiver) = outbound_channel(5);
1592        output.send(vec![0; 4], None).await.unwrap();
1593
1594        let cancellation = CancellationToken::new();
1595        let blocked = tokio::spawn({
1596            let output = output.clone();
1597            let cancellation = cancellation.clone();
1598            async move { output.send(vec![1; 4], Some(&cancellation)).await }
1599        });
1600        tokio::task::yield_now().await;
1601        assert!(!blocked.is_finished());
1602
1603        cancellation.cancel();
1604        assert_eq!(blocked.await.unwrap(), Err(OutboundSendError::Cancelled));
1605
1606        drop(receiver.recv().await.unwrap());
1607        output.send(vec![2; 4], None).await.unwrap();
1608    }
1609
1610    #[tokio::test]
1611    async fn outbound_control_send_times_out_under_byte_backpressure() {
1612        let (output, _receiver) = outbound_channel(5);
1613        output.send(vec![0; 4], None).await.unwrap();
1614        let result = output
1615            .send_with_timeout(vec![1; 4], None, Duration::from_millis(10))
1616            .await;
1617        assert_eq!(result, Err(OutboundSendError::TimedOut));
1618    }
1619
1620    #[tokio::test]
1621    async fn active_turn_shutdown_aborts_after_grace_period() {
1622        let cancellation = CancellationToken::new();
1623        let dropped = Arc::new(AtomicBool::new(false));
1624        let (started_tx, started_rx) = oneshot::channel();
1625        let task = tokio::spawn({
1626            let dropped = Arc::clone(&dropped);
1627            async move {
1628                let _signal = DropSignal(dropped);
1629                let _ = started_tx.send(());
1630                pending::<()>().await;
1631            }
1632        });
1633        started_rx.await.unwrap();
1634
1635        let graceful = shutdown_active_turn(
1636            ActiveTurn {
1637                turn_id: "turn".into(),
1638                cancellation,
1639                task,
1640            },
1641            Duration::from_millis(10),
1642        )
1643        .await;
1644
1645        assert!(!graceful);
1646        assert!(dropped.load(Ordering::Acquire));
1647    }
1648
1649    #[tokio::test]
1650    async fn writer_shutdown_aborts_after_grace_period() {
1651        let dropped = Arc::new(AtomicBool::new(false));
1652        let (started_tx, started_rx) = oneshot::channel();
1653        let writer = tokio::spawn({
1654            let dropped = Arc::clone(&dropped);
1655            async move {
1656                let _signal = DropSignal(dropped);
1657                let _ = started_tx.send(());
1658                pending::<std::io::Result<()>>().await
1659            }
1660        });
1661        started_rx.await.unwrap();
1662
1663        let result = shutdown_writer(writer, Duration::from_millis(10)).await;
1664
1665        assert!(result.is_err());
1666        assert!(dropped.load(Ordering::Acquire));
1667    }
1668}